🇬🇧 English · 🇪🇸 Español
Transition audits and bounded refinement
The result audit can inspect perfect-foresight and built-in one-asset nonlinear HANK paths. It evaluates supplied paths without choosing a replacement solution. A separate opt-in refinement ladder runs explicitly supplied callbacks within a declared work budget.
Editable HANKResult closures (closure='model') are also supported. The audit
re-evaluates the original aggregate equations and linear household Jacobian response
on the supplied paths. For nonlinear household closures it compares outputs with
the retained final household transition only after verifying that the supplied
household input paths still match that experiment. A changed input fails its context
check and leaves household consistency not_checked; no household solve runs.
Retained stationary mass is checked separately. Missing micro Euler, budget, and
distribution-transport evidence stays explicitly not_checked.
Perfect-foresight paths
from puremacro.validation import audit_result
result = model.solve_perfect_foresight(periods=80)
report = audit_result(result)
print(report.to_markdown())
errors_by_date_and_equation = report.maps["path_equations"]
New PerfectForesightResult objects retain their resolved initial and terminal
boundaries, exogenous path, variable order and prepared equation callable.
The audit evaluates each original equation at the supplied path, including its
two boundary dates. STEADY_STATE() uses the solved fixed reference. It never
re-solves an endval block or reconstructs shocks from a current model.
Residual maps have shape (period, equation), with periods numbered from one.
An altered trajectory fails even if its converged flag and recorded residual
still claim success. The reported distance between the final simulated date
and the terminal boundary is an ungraded horizon diagnostic: the boundary is
imposed at T+1, not at T.
For legacy or manually constructed results, supply the missing context:
from puremacro.validation import PathAuditContext
context = PathAuditContext(
equations=equations, # (lead, current, lag, shock) -> residual vector
y_init=initial_state,
y_end=terminal_state,
exogenous_path=shocks, # exactly one row per reported date
variable_names=result.variable_names,
)
report = audit_result(result, model=context)
If the callable explicitly declares a steady_state argument, it receives the
fixed terminal state. The caller supplies any parameter binding needed by a
raw callable. Without sufficient context the equation check is not_checked.
Rolling surprise paths and MCP paths have different expectation/complementarity
contracts and are not covered by this adapter. No probability-distribution
claim is made for a representative-agent perfect-foresight path.
Nonlinear HANK paths
from puremacro.models.hank_sequence_space import solve_nonlinear_transition
transition = solve_nonlinear_transition(
steady_state, shock_seq=monetary_shocks, horizon=80,
retain_audit_paths=True,
)
report = audit_result(transition)
budget_map = report.maps["household_budget"]
transport_map = report.maps["distribution_transport"]
Effective phi_pi, kappa and household parameters are retained, including
overrides that differ from the supplied steady-state object's parameters.
Even without household path retention, the audit recomputes the goods-market,
Phillips-curve and Taylor/Fisher identities from the reported paths. The
Phillips curve uses the model's zero terminal inflation deviation. These are
checks of the built-in closure, not an arbitrary .mod aggregate block.
retain_audit_paths=True stores consumption and savings policies, entering-date
and terminal distributions, realized returns, wages and the terminal consumption
policy from the already computed final household pass. It does not add an EGM
solve. Main retained arrays require approximately
8 * (3*T + 2) * n_assets * n_productivity bytes, plus small input arrays.
The default is False; unavailable household evidence is not_checked.
Audit maps themselves also consume memory proportional to the path size.
With retained paths, checks include:
- Resource budgets at every
(date, assets, productivity)point, positive consumption, asset-domain feasibility and the realized-return timing. - Independent aggregation of consumption and re-evaluation of goods-market clearing, including fiscal spending and the balanced interest-payment tax.
- Euler/KKT residuals using marginal utility interpolated at actual continuous savings policies. The final Euler equation uses the retained continuation consumption. Weighted percentiles weight dates equally and use each date's supplied probability mass; they are not stationary-distribution percentiles.
- Each date's total and negative mass, the initial distribution, policy-lottery transport, mean-asset preservation and mass at the artificial upper boundary.
Nothing normalizes an invalid distribution, clips a supplied policy or solves
for a better household. A 25-point, eight-date synthetic test clears equations
near 2e-12 while its Euler error is about 0.0023 and upper-bound mass about
0.0056; the latter fails the default 0.001 screen. Such observations describe
that calibration and grid, not a universal numerical error bound.
Explicit refinement ladders
Existing RefinementCheck callbacks compare one candidate with a baseline.
RefinementLadder instead compares adjacent resolutions, records every attempt,
and can stop before calling the remaining solvers:
from puremacro.validation import RefinementLadder, RefinementLevel
ladder = RefinementLadder(
"horizon",
levels=[
RefinementLevel("T160", solve_T160, settings={"horizon": 160}, cost=2),
RefinementLevel("T320", solve_T320, settings={"horizon": 320}, cost=4),
RefinementLevel("T640", solve_T640, settings={"horizon": 640}, cost=8),
],
metrics=extract_economic_metrics,
max_solves=3, max_cost=14, consecutive_passes=2,
rtol=0.005, atol=1e-8,
units={"consumption_window": "goods", "terminal_asset_gap": "assets"},
validate_pair=check_same_economic_experiment,
)
report = audit_result(
baseline,
checks=("solver", "equations", "distribution", "refinement"),
refinement_ladders=[ladder],
)
print(report.metadata["refinement_ladders"]["horizon"]["stop_reason"])
metrics(result) is mandatory and returns the same nonempty mapping of finite
scalars at every level. Choose quantities that answer the research question:
consumption in a fixed economic window, wealth or welfare, a terminal gap,
Euler error quantiles, or selected Jacobian entries. A callback may do expensive
work only because this experiment is explicitly selected. For incomparable
units, scale metrics deliberately or use separate ladders with different
tolerances. No metric, horizon or grid is inferred.
Every metric must meet abs(new-old) <= atol + rtol*abs(new). Success requires
the chosen number of consecutive passing comparisons; a failed comparison
resets the streak. A solver that declares non-convergence, a failed callback,
nonfinite metric, changed metric keys or rejected pair ends the ladder without
a stability claim. Previous failed comparisons remain visible after eventual
stabilization. Stability concerns only the selected metrics and resolutions.
validate_pair(previous, new) may raise if the economic specification,
boundaries or shock experiment changed. Without it, experiment equivalence
is explicitly not_checked; settings are caller-supplied provenance, not
an inspection of what the solve callback actually did.
max_solves limits attempted callbacks, including failures. Each attempted
callback consumes its declared positive cost; a callback is not started if
its cost would exceed max_cost. These work units are not elapsed seconds,
memory, or a hard execution timeout. The ladder cannot interrupt a running
callback. Stops distinguish stable_metrics, solve_budget_exhausted,
cost_budget_exhausted, levels_exhausted, and named computation failures.
The report retains budgets, attempts, metric values and stopping evidence in
deterministic JSON, Markdown and DataFrame metadata. Merely supplying a ladder
without selecting refinement executes no metric or solve callback.