Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions connect/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ <h2>Connect in three steps.</h2>
<div class="prompt-block">
<div class="meth-sec-title">example prompts</div>
<ul class="prompt-list">
<li class="prompt-card"><span class="prompt-tool">calculate_household</span><code>How much tax would a single 35-year-old earning $50,000 pay in the US?</code><button data-copy>copy</button></li>
<li class="prompt-card"><span class="prompt-tool">calculate_household</span><code>How much tax would a single 35-year-old earning $50,000 pay in Los Angeles County, California?</code><button data-copy>copy</button></li>
<li class="prompt-card"><span class="prompt-tool">score_reform</span><code>Score raising the UK basic rate of income tax to 21p.</code><button data-copy>copy</button></li>
<li class="prompt-card"><span class="prompt-tool">forecast_uk</span><code>What is the UK GDP and inflation outlook for the next four quarters?</code><button data-copy>copy</button></li>
<li class="prompt-card"><span class="prompt-tool">frbus_shock</span><code>What happens to US GDP and unemployment if the Fed raises rates by 100 basis points?</code><button data-copy>copy</button></li>
Expand Down Expand Up @@ -390,7 +390,7 @@ <h2>Run the shared CLI.</h2>
<div class="prose">
<div class="copyblock"><code>pip install "policyengine-macro[models] @ git+https://github.com/PolicyEngine/macro#subdirectory=integration"</code><button data-copy>copy</button></div>
<div class="command-grid">
<div><span class="dial-name">Household calculation</span><div class="copyblock"><code>pe-macro household --country us --people '[{"age":35,"employment_income":50000}]'</code><button data-copy>copy</button></div></div>
<div><span class="dial-name">Household calculation</span><div class="copyblock"><code>pe-macro household --country us --people '[{"age":35,"employment_income":50000}]' --household '{"state_code":"CA","county_fips":"06037"}'</code><button data-copy>copy</button></div></div>
<div><span class="dial-name">Score a UK reform</span><div class="copyblock"><code>pe-macro score --country uk --reform '{"gov.hmrc.income_tax.rates.uk[0].rate":0.21}' --model obr</code><button data-copy>copy</button></div></div>
<div><span class="dial-name">UK forecast</span><div class="copyblock"><code>pe-macro forecast</code><button data-copy>copy</button></div></div>
<div><span class="dial-name">US rate shock</span><div class="copyblock"><code>pe-macro frbus-shock --var rffintay_aerr --shock 1.0</code><button data-copy>copy</button></div></div>
Expand Down
6 changes: 5 additions & 1 deletion economy/topics.py
Original file line number Diff line number Diff line change
Expand Up @@ -1376,6 +1376,9 @@ def route_row(title: str, chain: list[str], line: str, comment: str) -> str:
UK_REFORM = '{"gov.hmrc.income_tax.rates.uk[0].rate":0.21}'
US_REFORM = '{"gov.irs.credits.ctc.amount.adult_dependent":1000}'
PEOPLE = '[{"age":35,"employment_income":50000}]'
# policyengine-us 2.x needs a county (or an explicit SPM selection) for every
# US household result; a state alone is rejected.
US_HOUSEHOLD = '{"state_code":"CA","county_fips":"06037"}'


def uk_platform() -> str:
Expand Down Expand Up @@ -1475,7 +1478,8 @@ def us_platform() -> str:
route_row(
"One household, exactly",
["pe-microsim"],
f"pe-macro household-impact --country us --people '{PEOPLE}' --reform '{US_REFORM}'",
f"pe-macro household-impact --country us --people '{PEOPLE}' "
f"--household '{US_HOUSEHOLD}' --reform '{US_REFORM}'",
"arithmetic over the statutory rules; no sampling, no weights",
),
route_row(
Expand Down
2 changes: 1 addition & 1 deletion forecasts/us/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ <h2>Four outputs, and the two that do not exist.</h2>
<caption>What this section can produce for the US. Every command is checked against <code>cli.py</code> when this page is generated.</caption>
<thead><tr><th scope="col">What you get</th><th scope="col">Models, in the order they run</th><th scope="col">What the chain does</th><th scope="col">Command</th></tr></thead>
<tbody>
<tr><th scope="row">One household, exactly</th><td><a href="/pe">pe-microsim</a></td><td>arithmetic over the statutory rules; no sampling, no weights</td><td><code class="mono">pe-macro household-impact --country us --people &#x27;[{&quot;age&quot;:35,&quot;employment_income&quot;:50000}]&#x27; --reform &#x27;{&quot;gov.irs.credits.ctc.amount.adult_dependent&quot;:1000}&#x27;</code></td></tr>
<tr><th scope="row">One household, exactly</th><td><a href="/pe">pe-microsim</a></td><td>arithmetic over the statutory rules; no sampling, no weights</td><td><code class="mono">pe-macro household-impact --country us --people &#x27;[{&quot;age&quot;:35,&quot;employment_income&quot;:50000}]&#x27; --household &#x27;{&quot;state_code&quot;:&quot;CA&quot;,&quot;county_fips&quot;:&quot;06037&quot;}&#x27; --reform &#x27;{&quot;gov.irs.credits.ctc.amount.adult_dependent&quot;:1000}&#x27;</code></td></tr>
<tr><th scope="row">The whole population, one policy year</th><td><a href="/pe">pe-microsim</a></td><td>revenue and distribution; a static costing</td><td><code class="mono">pe-macro score --country us --reform &#x27;{&quot;gov.irs.credits.ctc.amount.adult_dependent&quot;:1000}&#x27; --model microsim</code></td></tr>
<tr><th scope="row">A macro shock carried into household earnings</th><td><a href="/frb-us">frb-us</a> &rarr; <a href="/pe">pe-microsim</a></td><td>the wage-bill change applied through the automatic stabilisers</td><td><code class="mono">pe-macro frbus-shock-incidence --var rffintay_aerr --shock 1.0 --year 2027</code></td></tr>
<tr><th scope="row">The same question in the HANK member</th><td><a href="/us-hank">us-hank</a> &rarr; <a href="/pe">pe-microsim</a></td><td>impulse responses around a calibrated steady state</td><td><code class="mono">pe-macro hank-shock-incidence --kind monetary --size -0.0025 --year 2026</code></td></tr>
Expand Down
29 changes: 22 additions & 7 deletions integration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ one CLI and one MCP server:
model — monetary, fiscal, and productivity shock experiments via
`hank_shock`, with distributional readouts (MPC by wealth quartile).
Does not score reforms.
- **PolicyEngine microsimulation** (`policyengine` from PyPI, v4): full
- **PolicyEngine microsimulation** (`policyengine` from PyPI, 6.11.1, with
policyengine-us 2.2.1 and policyengine-uk 2.151.0): full
UK/US tax-benefit rules — household calculator, baseline-vs-reform
impacts, and population-level reform scoring over representative
microdata (UK data is private: set `HUGGING_FACE_TOKEN`; the hosted
deployment provisions it server-side).
impacts, and population-level reform scoring over the microdata that
policyengine release certifies (UK data is private: set
`HUGGING_FACE_TOKEN`; the hosted deployment provisions it server-side).
A US household needs a Supplemental Poverty Measure geography: its
five-digit `county_fips`, or an explicit `spm` selection such as
`{"geography_kind": "national"}`. There is no default.
- **OG-UK** (`oguk`, optional/local-only): overlapping-generations
steady-state scoring through `pe-macro score --model og`.

Expand Down Expand Up @@ -190,13 +194,24 @@ pe-macro household --country uk \
pe-macro household-impact --country uk \
--people '[{"age":35,"employment_income":50000}]' \
--reform '{"gov.hmrc.income_tax.rates.uk[0].rate":0.25}'
pe-macro household --country us \
--people '[{"age":35,"employment_income":50000}]' \
--household '{"state_code":"CA","county_fips":"06037"}'
pe-macro household --country us \
--people '[{"age":35,"employment_income":50000}]' \
--household '{"state_code":"CA"}' --spm '{"geography_kind":"national"}'
pe-macro population-impact --country uk \
--reform '{"gov.hmrc.cgt.basic_rate":0.20,"gov.hmrc.cgt.higher_rate":0.40}'
```

`--people`/`--benunit`/`--tax-unit`/`--household`/`--reform` take JSON.
Money amounts are annual GBP (uk) or USD (us); reform rates are decimals
(0.25 = 25%). The first PolicyEngine call in a process pays a ~20 s model
`--people`/`--benunit`/`--tax-unit`/`--household`/`--spm`/`--reform` take
JSON. Money amounts are annual GBP (uk) or USD (us); reform rates are
decimals (0.25 = 25%). A US household needs either a `county_fips` (five
digits, as a string, in the household's `state_code`) or an `--spm`
selection: policyengine-us 2.x computes the Supplemental Poverty Measure in
every household result, and a state alone does not identify an SPM area.
The same choice is required by policyengine.py, the PolicyEngine API and
the PolicyEngine web app. The first PolicyEngine call in a process pays a ~20 s model
import (it is loaded lazily).

Add `--json` to any command for machine-readable output. Units: CGG/CGIPS
Expand Down
31 changes: 25 additions & 6 deletions integration/modal_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,16 @@
# imports it lazily inside the pe_* adapters — module import at
# container start stays fast; only the first policyengine tool call
# in a fresh container pays the load.
"policyengine[models]>=4,<5", # [models] extra REQUIRED: base install leaves pe.uk/pe.us as None
# policyengine's bundled data-release manifest certifies an exact
# country-package version; a newer policyengine-us makes every US
# (and via import, UK) call raise at import time. Keep this pin in
# sync with the manifest version reported by the certification error.
"policyengine-us==1.764.6",
"policyengine[models]==6.11.1", # [models] extra REQUIRED: base install leaves pe.uk/pe.us as None
# The country packages policyengine 6.11.1's [models] extra pins.
# policyengine's bundled data-release manifest certifies the US and
# UK microdata for exactly these versions; with any other
# policyengine-us, `import policyengine` fails certification and
# every US and UK call raises. Keep all four equal to the [models]
# extra in integration/pyproject.toml.
"policyengine-us==2.2.1",
"policyengine-uk==2.151.0",
"policyengine-core==3.33.3",
# US HANK (hank_shock / hank_summary). Installed from GitHub as a
# normal wheel — no data files or repo checkout needed at runtime;
# the calibration is code. Cost profile: the first hank_shock call
Expand Down Expand Up @@ -284,6 +288,21 @@
f"pip install -e {OBR_REPO} -e {BOE_REPO} -e {FRB_REPO} "
f"-e {HOME}/macro/integration"
)
# Refresh the two CA lists that the server's outbound HTTPS (Hugging Face
# microdata downloads, PolicyEngine data fetches) verifies against:
# Debian's ca-certificates, read by urllib, aiohttp and hf-xet, and certifi,
# read by requests and httpx. The pip_install layer built on 2026-08-12
# held certifi 2026.7.22, on a debian_slim base built before Debian
# released ca-certificates 20250419~deb12u2; both lists trusted the
# compromised eMudhra root "emSign ECC Root CA - G3"
# (https://emudhra.we-have-a-problem.com).
# The pip_install layer now rebuilds (its policyengine pins changed), which
# would pull the current certifi on its own; these two explicit steps
# keep the fixed minimums enforced for any later rebuild, whatever the
# layers above resolve to, and refresh the Debian package regardless of
# how old Modal's cached debian_slim base layer is.
.apt_install("ca-certificates")
.pip_install("certifi>=2026.10.19")
)

app = modal.App("policyengine-macro-mcp")
Expand Down
21 changes: 14 additions & 7 deletions integration/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,20 @@ models = [
"boe_var @ git+https://github.com/PolicyEngine/boe-var-model@42cbcbb728b55b3f416764568fa8f3aef68a6730",
"frbus @ git+https://github.com/PolicyEngine/us-frb-model@2508a2d3ee7d078fd82f38bc17567e4433489913",
"us-hank-model @ git+https://github.com/PolicyEngine/us-hank-model@19a8e5e5cd8e7120fbbd2e3de3c5b63509824f3c",
# [models] pulls the pinned policyengine-uk / policyengine-us country
# packages that pe.uk / pe.us need at runtime.
"policyengine[models]>=4,<5",
# policyengine's bundled data-release manifest certifies an exact
# policyengine-us version; anything newer fails certification at import
# (every pe.* call raises). Keep in sync with the manifest pin.
"policyengine-us==1.764.6",
# policyengine (the policyengine.py package) with its [models] extra,
# which pins the country packages that pe.uk / pe.us run. policyengine
# 6.x needs Python >= 3.11.
"policyengine[models]==6.11.1",
# The country packages policyengine 6.11.1's [models] extra pins, listed
# explicitly so that bumping policyengine alone fails to resolve instead
# of silently changing the models. policyengine's bundled data-release
# manifest certifies the US and UK microdata for exactly these versions;
# with any other policyengine-us, `import policyengine` fails
# certification and every pe.* call (UK included) raises. Change these
# four pins together, and keep them equal to integration/modal_app.py.
"policyengine-us==2.2.1",
"policyengine-uk==2.151.0",
"policyengine-core==3.33.3",
]

[project.scripts]
Expand Down
13 changes: 10 additions & 3 deletions integration/src/policyengine_macro/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -742,7 +742,12 @@ def _pe_common_options(fn):
click.option("--tax-unit", "tax_unit", default=None,
help='US only: JSON tax unit dict, e.g. \'{"filing_status":"SINGLE"}\'.'),
click.option("--household", default=None,
help='JSON household dict, e.g. \'{"state_code_str":"CA"}\' (US).'),
help='JSON household dict, e.g. '
'\'{"state_code":"NY","county_fips":"36061"}\' (US).'),
click.option("--spm", default=None,
help='US only: JSON SPM geography selection, e.g. '
'\'{"geography_kind":"national"}\'. A US household '
'needs this or a county_fips in --household.'),
click.option("--json", "as_json", is_flag=True, help="Emit JSON."),
]):
fn = opt(fn)
Expand All @@ -762,7 +767,7 @@ def _echo_summary(label: str, summary: dict, sym: str) -> None:
@main.command()
@_pe_common_options
@click.option("--reform", default=None, help=_REFORM_HELP)
def household(country, people, year, benunit, tax_unit, household, as_json, reform):
def household(country, people, year, benunit, tax_unit, household, spm, as_json, reform):
"""Calculate taxes and benefits for a household (PolicyEngine)."""
res = core.pe_household(
country=country,
Expand All @@ -772,6 +777,7 @@ def household(country, people, year, benunit, tax_unit, household, as_json, refo
benunit=_json_opt(benunit, "benunit"),
tax_unit=_json_opt(tax_unit, "tax-unit"),
household=_json_opt(household, "household"),
spm=_json_opt(spm, "spm"),
)
if as_json:
_emit_json(res)
Expand All @@ -785,7 +791,7 @@ def household(country, people, year, benunit, tax_unit, household, as_json, refo
@main.command("household-impact")
@_pe_common_options
@click.option("--reform", required=True, help=_REFORM_HELP)
def household_impact(country, people, year, benunit, tax_unit, household, as_json, reform):
def household_impact(country, people, year, benunit, tax_unit, household, spm, as_json, reform):
"""Baseline-vs-reform impact of a reform on one household (PolicyEngine)."""
res = core.pe_household_impact(
country=country,
Expand All @@ -795,6 +801,7 @@ def household_impact(country, people, year, benunit, tax_unit, household, as_jso
benunit=_json_opt(benunit, "benunit"),
tax_unit=_json_opt(tax_unit, "tax-unit"),
household=_json_opt(household, "household"),
spm=_json_opt(spm, "spm"),
)
if as_json:
_emit_json(res)
Expand Down
Loading
Loading