Skip to content

🇬🇧 English · 🇪🇸 Español

A workbench for DSGE experiments

DSGEWorkbench keeps a model, its draft calibration and its saved runs together. It runs the existing first-order IRF and deterministic perfect-foresight engines. Notebook displays show the run state, exact inputs and separate accuracy checks; they do not select a different mathematical problem automatically.

Solve, save and compare

import numpy as np
from puremacro.dsge import DSGEWorkbench, DSGERun

source = """
var x; varexo e; parameters rho; rho=.8;
model; [name='Persistence'] x=rho*x(-1)+e; end;
initval; x=0; end;
"""
bench = DSGEWorkbench.from_mod(source, name="Persistence experiment")
baseline = bench.irf("e", horizon=20, size=.01, label="Baseline")
alternative = bench.irf("e", horizon=20, size=.01,
                         params={"rho": .5}, label="Lower persistence")
assert baseline.status == alternative.status == "succeeded"
np.testing.assert_allclose(baseline.table().x, .01 * .8 ** np.arange(21))
comparison = baseline.compare(alternative)
print(comparison.to_frame())
print(comparison.settings().query("changed"))

Display bench, baseline, baseline.accuracy or comparison in a notebook for HTML tables with expandable evidence. comparison.plot() overlays the two saved paths. baseline.plot() and baseline.accuracy.plot() produce Matplotlib figures. These are views of captured data; editing parameters later does not change them. to_dict(), table() and the evidence accessors return copies.

from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as directory:
    directory = Path(directory)
    path = baseline.save(directory / "baseline.json")
    saved = DSGERun.load(path)  # verifies checksums; executes no model
    replayed = saved.replay()  # explicitly solves the frozen recipe again
    np.testing.assert_allclose(replayed.table(), baseline.table())
    (directory / "baseline.py").write_text(saved.to_python(), encoding="utf-8")
    (directory / "baseline.html").write_text(saved.to_html(), encoding="utf-8")
    (directory / "comparison.html").write_text(comparison.to_html(), encoding="utf-8")

Archives retain the expanded .mod source, hashes of its original source files, calibration, shock specification, solver options, units, timing, run identifier, environment fingerprint, numeric tables and accuracy evidence. Included source files may subsequently change without changing the saved recipe. Existing files require overwrite=True; loading rejects damaged checksums and archives larger than 32 MiB. Checksums detect accidental corruption, not author identity.

Replay checks the recorded Python/package versions and a source fingerprint of PureMacro. A changed environment requires allow_environment_change=True and produces a new run. The exported Python is an editable recipe with recorded environment comments; executing it does not enforce this environment guard. Loading an archive never executes its recipe. Callback models and explicitly captured external results are snapshots without portable replay or Python export.

Comparisons require successful runs with the same ordered variables, declared units, timing and dates. Different dates require alignment="inner", which reports the common window and both original lengths. Different shocks or boundary conditions require allow_experiment_change=True. Calibration and method changes remain visible in settings(). A difference between two runs is not, by itself, a convergence or welfare test.

Apply edits explicitly

widget = bench.interactive_irf(parameters={"rho": (.1, .95)}, shocks="e",
                               horizon=20, size=.01)
widget.update({"rho": .65})
assert widget.run_state == "edited"
assert widget.to_frame().isna().all().all()
widget.apply()
assert widget.is_current
snapshot = widget.capture_run(label="Selected slider experiment")
np.testing.assert_allclose(snapshot.table().x, .01 * .65 ** np.arange(21))
widget.disconnect()

The workbench opens the existing Matplotlib interface in Apply mode. Slider changes mark it edited and clear the active curve until Apply succeeds. Set auto_solve=True to solve each edit; direct model.interactive_irf(...) retains that default for compatibility. A batched update({...}) solves once. Reset restores the initial slider settings and follows the selected Apply mode.

The static baseline remains labeled as a reference. Failed solves clear active curves and exports; last_success_model retains the previous success explicitly. Errors distinguish steady states, determinacy, input/domain and numerical failures and suggest the next diagnostic. A failed initial override is visible too. capture_run() freezes selected slider outcomes as snapshots; it does not append every mouse movement to workbench history or invent a portable model recipe.

Live dragging and buttons require an interactive Matplotlib backend, such as a supported notebook widget backend or a desktop GUI. Inline/Agg output is static; the Python methods still work. The HTML reports are inspectable saved reports, not a background execution service.

Inspect each accuracy dimension

run.status == "succeeded" means that the selected engine completed with its convergence criterion. run.accuracy separately reports equations, determinacy, feasibility, household grids, horizon, quadrature, compression, branching and derivatives. There is deliberately no overall accuracy pass flag.

Evidence state Meaning
passed / failed A named check evaluated against its displayed criterion
reported A value or diagnostic was retained without a new acceptance test
not_checked No evidence for this dimension was supplied or evaluated

The dashboard reuses audit_result where supported. First-order equation checks do not certify global stochastic accuracy. Higher-order captures audit only their embedded first-order model. Terminal distance is reported separately from a horizon comparison. Global policy results retain their pointwise policy audit; they do not gain a grid-refinement certificate from iteration convergence.

For HANK results, the adapter labels existing solver diagnostics as recorded evidence. Euler, compression, mass and budget metrics need explicit tolerances to become numerical passes. Missing diagnostics remain unchecked. Capturing an external result requires the caller to supply its experiment metadata; the workbench cannot recover arbitrary callback code or omitted calibration details.

from puremacro.dsge import solution_accuracy

checks = solution_accuracy(None)
assert set(checks.to_frame().status) == {"not_checked"}

For an existing HANK result, use solution_accuracy(result, tolerances={"euler": 1e-3, "compression": 1e-4}) with thresholds justified by your experiment. Capture it with DSGERun.capture(result, parameters=..., configuration=..., units=..., timing=...), or add it to history with bench.record(...). The thresholds above illustrate the API and are not recommended universal accuracy requirements.

Progress, cancellation and recovery

from puremacro.dsge import DSGERunControl

events = []
control = DSGERunControl(max_seconds=30, on_progress=events.append)
path = bench.perfect_foresight(periods=5, y_init=[0.], y_end=[0.],
                               shocks=[[.01], [0.], [0.], [0.], [0.]],
                               linear_solver="krylov", control=control)
assert path.status == "succeeded"
assert len(events) > 0

control.cancel() requests cooperative cancellation. Krylov solves check at accepted Newton steps and existing inner progress hooks; other operations check at stage boundaries. Calls remain synchronous, and max_seconds is a cooperative solver budget, not a hard deadline. The final evidence capture may take additional time. A UI running the call on its main thread cannot process a cancel button until that thread yields; use a separate execution context or a progress callback.

Cancellation and budget exhaustion retain distinct states (cancelled and budget_exhausted). If a Krylov step was accepted, the archive retains its path checkpoint and matching input fingerprint. stopped_run.replay(resume=True) uses that path as an initial guess and reruns all solver checks. It does not restore Krylov internals or certify an interrupted path as solved. A new control can set a different budget for the resumed run. Without a compatible accepted checkpoint, resume is rejected.

The runnable example python -m puremacro.examples.dsge_workbench compares two analytically known AR(1) responses. Add --out DIRECTORY to export its archives, Python recipe, HTML reports and plots. See DSGE workflows for the mathematical scope of the other solvers.