Skip to content

Host the FRB/US model on the PolicyEngine Macro MCP server#57

Merged
vahid-ahmadi merged 1 commit into
mainfrom
frbus-hosted
Jul 19, 2026
Merged

Host the FRB/US model on the PolicyEngine Macro MCP server#57
vahid-ahmadi merged 1 commit into
mainfrom
frbus-hosted

Conversation

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

FRB/US was local-only. This bakes it into the Modal image and exposes it as three MCP tools plus three CLI commands, taking the hosted tool surface from 10 to 13.

Tools added

tool signature
frbus_shock (var, shock, start="2026Q1", periods=1, horizon=20, policy_rule="inertial_taylor", variables=None, name=None) -> dict
frbus_list_variables () -> list[dict]
frbus_summary () -> dict

CLI mirrors: pe-macro frbus-shock / frbus-variables / frbus-summary.

frbus_shock solves an add-factored baseline that reproduces LONGBASE to machine precision, then the shocked model, and returns per-quarter deviations for xgdp, lur, picxfe, pcpi, rff plus any requested extras, with a peaks block and per-series units.

policy_rule is exposed because the answer materially depends on it: inertial_taylor (default), taylor, or fixed_funds_rate (rff exogenized — no endogenous monetary offset, so fiscal multipliers are larger).

Two silent-wrong-answer traps made loud

Both are the same failure mode as the OBR emulator's investment_closure trap, and get the same treatment:

  • Rule/lever mismatch. Each policy rule reads its own add-error. Shocking rffintay_aerr under a non-inertial rule converges and returns exactly zero everywhere — which reads as "monetary policy does nothing". It now raises and names the right lever, with a backstop warning for any other all-zero solve.
  • Units. FRB/US equations mix levels, rates and log-differences, so <var>_aerr inherits its own equation's units. egfe_aerr is in log points of quarterly growth, not billions of dollars; a dollar-sized shock diverges the solver. Units are documented per lever, and a divergence surfaces as a units error rather than a raw solver traceback.

Scope: no reform bridge, deliberately

score_reform refuses model='frbus'. There is no mapping today from a PolicyEngine US reform to FRB/US fiscal levers, so anything it returned would be a plausible-looking wrong number. The error names frbus_shock as the supported entry point; the docstrings and frbus_summary().reform_bridge say the same.

Image

us-frb-model is baked in at the same absolute path as the other two repos and pip install -e'd — model.xml and LONGBASE.TXT live in the repo's vendor/ and are not package data, so a wheel install imports fine but has no model to run.

Only those two files (4.9MB) are needed at runtime. Excluded: the vendor .zip archives (which duplicate their own unpacked contents), the EViews-era frbus_package, the pyfrbus reference implementation and its docs, the EViews database, HISTDATA.TXT, tests/ and scripts/. 251MB -> 5.2MB. The MACROMOD_IMAGE_SOURCE=github branch prunes equivalently and then asserts both files survive, so a bad exclusion fails the build rather than the first production request.

Also excludes **/.venv from all three repo copies: a developed checkout carries its own virtualenv (213MB in us-frb-model), which add_local_dir would otherwise copy into the image, shadowing container site-packages with laptop-built wheels.

Measured latency (laptop)

call time
frbus_shock cold (first in process) ~3s (0.1s data + 0.7s symbolic compile + 2.3s init_trac + 0.2-0.5s solve)
frbus_shock warm (cached baseline) ~0.5s
frbus_list_variables instant
frbus_summary instant after the one-off ~3s frbus import

Against the 600s Modal timeout that is ~200x headroom. _FRBUS_BASELINE_CACHE holds the compiled model and its baseline per (policy_rule, start, end). The COST PROFILE block in modal_app.py is updated with memory/CPU implications and why FRB/US is hostable where OG-UK is not (bounded linear-time simulation vs an unbounded nested fixed-point steady-state search).

Verification

Exercised end to end in a scratch venv against both a full checkout and a pruned copy matching the image exclusions — identical results, matching VALIDATION.md exactly:

xgdp trough  -0.5512%  in 2027Q4   (documented -0.55%)
lur peak     +0.2584pp in 2027Q4   (documented +0.26pp)
picxfe       -0.0336pp             (documented -0.034pp)
rff impact   +1.0001pp             (documented +1.00pp)

Tracking invariant after init_trac: 5.6e-17.

import policyengine_macro.mcp_server stays fast (~1.4s) and does not import frbus — the lazy-import property is preserved.

Tests

tests/test_frbus.py, 22 tests, following the existing fast/slow marker layout. The slow ones are the deployment gates: model and data files resolve after pruning, the tracking invariant holds to the model repo's published 1e-8 gate, the 100bp shock reproduces the documented signs and magnitudes, and fixed_funds_rate amplifies a demand shock relative to the Taylor rule (which would fail if the rule switch were silently ignored). Magnitudes use wide bands against VALIDATION.md rather than exact floats, so a LONGBASE re-vintage does not break the build while a sign flip or decoupled lever does.

tests/test_frbus.py tests/test_tool_surface.py --runslow  ->  39 passed
full fast suite                                          ->  120 passed, 61 skipped

One pre-existing failure, test_core.py::test_summary_parses, is unrelated — it needs boe_var, which is absent from my scratch venv; it fails identically on a clean main.

GOLDEN_TOOL_COUNT goes 10 -> 13 in tests/tool_surface.py; the remote-deployment test picks this up automatically.

Not touched: any site .html, style.css, sitemap.xml or vercel.json — a separate pass updates the site copy once this is live. No modal deploy run; CI deploys on merge.

🤖 Generated with Claude Code

FRB/US was local-only. It is now baked into the Modal image and exposed as
three MCP tools plus three CLI commands, taking the hosted tool surface from
10 to 13.

New tools (mirroring the obr_shock / list_reform_variables / model_summary
surface):

- frbus_shock(var, shock, start, periods, horizon, policy_rule, variables,
  name) — solves an add-factored baseline that reproduces LONGBASE to machine
  precision, then the shocked model, returning per-quarter deviations for
  xgdp, lur, picxfe, pcpi and rff plus any requested extras.
- frbus_list_variables() — the shockable levers with their units.
- frbus_summary() — model metadata and validation provenance.

CLI: pe-macro frbus-shock / frbus-variables / frbus-summary.

policy_rule is exposed because the answer materially depends on it:
'inertial_taylor' (default), 'taylor', or 'fixed_funds_rate' (rff exogenized,
no endogenous monetary offset, so fiscal multipliers are larger).

Two silent-wrong-answer traps are made loud rather than plausible:

- Each policy rule reads its OWN add-error. Shocking rffintay_aerr under a
  non-inertial rule converges and returns exactly zero everywhere, which reads
  as "monetary policy does nothing". It now raises and names the right lever.
  A backstop warning covers any other all-zero solve.
- FRB/US equations mix levels, rates and log-differences, so `<var>_aerr`
  inherits its own equation's units. egfe_aerr is log points of quarterly
  growth, not billions of dollars; a dollar-sized shock diverges the solver.
  Units are documented per lever and a divergence is reported as a units error.

SCOPE: score_reform deliberately REFUSES model='frbus'. No mapping exists
today from a PolicyEngine US reform to FRB/US fiscal levers, so anything it
returned would be a plausible-looking wrong number. The error names
frbus_shock as the supported entry point, and the docstrings say so.

Image: us-frb-model is baked in at the same absolute path as the other two
repos and pip install -e'd, since model.xml and LONGBASE.TXT live in vendor/
and are not package data — a wheel install imports but has no model to run.
Only those two files (4.9MB) are needed at runtime, so the vendor .zip
archives (which duplicate their own unpacked contents), the EViews-era
frbus_package, the pyfrbus reference implementation and its docs, the EViews
database, HISTDATA, tests/ and scripts/ are excluded: 251MB -> 5.2MB. The
github-source branch prunes equivalently and asserts both files survive, so a
bad exclusion fails the BUILD rather than the first production request.

Also excludes **/.venv from all three repo copies: a developed checkout
carries its own virtualenv (213MB in us-frb-model), which add_local_dir would
otherwise copy into the image, shadowing container site-packages with
laptop-built wheels.

Tests (tests/test_frbus.py, 22 tests) gate what a broken deployment would
break: the model and data files resolve after pruning, the tracking invariant
holds to the model repo's published 1e-8 gate, a 100bp rffintay_aerr shock
reproduces the documented signs and magnitudes, and the fixed-funds-rate rule
amplifies a demand shock relative to the Taylor rule. Magnitudes are asserted
with wide bands against VALIDATION.md rather than exact floats, so a LONGBASE
re-vintage does not break the build while a sign flip or a decoupled lever
does.

Verified locally against both a full checkout and a pruned copy matching the
image exclusions: identical results (xgdp trough -0.5512% in 2027Q4, lur peak
+0.2584pp, rff impact +1.0001pp), matching VALIDATION.md. Measured latency:
~3s cold, ~0.5s warm (the compiled model and its baseline are cached per
policy rule); list/summary are instant. Importing mcp_server stays fast and
does not import frbus.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
macromod Ready Ready Preview, Comment Jul 19, 2026 12:01pm

Request Review

@vahid-ahmadi
vahid-ahmadi merged commit bd20348 into main Jul 19, 2026
3 checks passed
@vahid-ahmadi
vahid-ahmadi deleted the frbus-hosted branch July 19, 2026 12:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant