Repository navigation
Conversation
Three guides under docs/, asked for at the 2026-10-08 BLS Consumer Expenditure seminar. Their reference tables are generated from the code by scripts/generate_docs_reference.py, and tests fail when they drift. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed tables Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Temporary files: deletion is attempted, not guaranteed; test leftovers. - Pre-2021 records: state checks do not apply, state -1 returns a wrong answer, idtl 5 does not work, marginal rates are 0. - Maryland: the bundled TAXSIM build includes county tax. - tfica includes employee state payroll taxes on PolicyEngine rows. - Linux TAXSIM build needs glibc 2.34 (generated from the binary). - Examples: do not round identifiers; Stata stores IDs as double and checks inputs before running. - Say what the network test covers and what it cannot see. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
#1422 makes the R package route by year like the CLI ( The two PRs touch no common file. Whichever merges second carries these edits. The patch applies to this branch at 2d3be9a with Patch (3 files, +11/−8)diff --git a/docs/design.md b/docs/design.md
index b31f739c..fbf9d24d 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -37,7 +37,7 @@ flowchart LR
| Bundled TAXSIM-35 | `resources/taxsimtest/`, `resources/taxsim35/` | NBER executables, installed with the package |
| Comparison | `policyengine_taxsim/comparison/`, `dashboard/` | Record-by-record comparison of the two engines, and the validation dashboard at policyengine.org/us/taxsim |
| Hosted API | `policyengine_taxsim/api.py` | The service behind the web runner, deployed on Modal; not used by a local install |
-| R package | `r-package/policyenginetaxsim/` | Calls `PolicyEngineRunner` through reticulate |
+| R package | `r-package/policyenginetaxsim/` | Calls `StitchedRunner` through reticulate |
The tax rules themselves live in [policyengine-us](https://github.com/PolicyEngine/policyengine-us), which this package installs as a dependency. The emulator chooses inputs and reads outputs; it does not compute taxes itself.
@@ -50,7 +50,7 @@ The tax rules themselves live in [policyengine-us](https://github.com/PolicyEngi
Each engine gets its records in one batch. The results are concatenated and put back in input order by original position, so repeated `taxsimid` values (panels, multi-state runs) keep their order. Options that only PolicyEngine understands (`--disable-salt`, `--assume-w2-wages`, `--logs`, an active S-corporation treatment) are applied to PolicyEngine rows only, with a warning when some records go to TAXSIM-35.
-The stdin command and the `policyengine` subcommand use `StitchedRunner`, as does the hosted web runner. `PolicyEngineRunner` used directly (the documented Python entry point) and the R package do not route: they compute every year with PolicyEngine. The `taxsim` subcommand computes every year with TAXSIM-35. The [input guide](input-guide.md#which-engine-computes-each-record) has the full table.
+The stdin command, the `policyengine` subcommand and the R package's `policyengine_calculate_taxes()` use `StitchedRunner`, as does the hosted web runner. The R function's `engine = "policyengine"` passes `pe_min_year=0`, which sends every record to PolicyEngine. `PolicyEngineRunner` used directly (the documented Python entry point) does not route: it computes every year with PolicyEngine. The `taxsim` subcommand computes every year with TAXSIM-35. The [input guide](input-guide.md#which-engine-computes-each-record) has the full table.
NBER's `taxsim35` Stata command is not part of this package. It sends every record to TAXSIM-35, and the copy NBER served on 2026-10-09 makes no reference to PolicyEngine. No interface switches engines except the ones built on `StitchedRunner`.
diff --git a/docs/input-guide.md b/docs/input-guide.md
index 0890e3ed..22910b6a 100644
--- a/docs/input-guide.md
+++ b/docs/input-guide.md
@@ -55,11 +55,12 @@ The emulator has two engines. PolicyEngine US computes 2021 and later. A TAXSIM-
| `policyengine-taxsim policyengine in.csv -o out.csv` | PolicyEngine | Bundled TAXSIM-35 |
| Python `StitchedRunner(df).run()` | PolicyEngine | Bundled TAXSIM-35 |
| Python `PolicyEngineRunner(df).run()` | PolicyEngine | PolicyEngine |
-| R `policyengine_calculate_taxes()` | PolicyEngine | PolicyEngine |
+| R `policyengine_calculate_taxes()` | PolicyEngine | Bundled TAXSIM-35 |
+| R `policyengine_calculate_taxes(engine = "policyengine")` | PolicyEngine | PolicyEngine |
| `policyengine-taxsim taxsim in.csv` | Bundled TAXSIM-35 | Bundled TAXSIM-35 |
| Web runner at policyengine.org/us/taxsim/run | PolicyEngine | TAXSIM-35, both on PolicyEngine's server |
-`PolicyEngineRunner` and the R package compute years before 2021 with PolicyEngine, which this project validates against TAXSIM only from 2021 on. Use the command line or `StitchedRunner` when your data include earlier years.
+`PolicyEngineRunner` computes years before 2021 with PolicyEngine, which this project validates against TAXSIM only from 2021 on, and so does the R package when asked to with `engine = "policyengine"`. Use the command line, `StitchedRunner` or the R package's default when your data include earlier years. The R package switched engines by year from version 0.2.0; earlier versions computed every year with PolicyEngine.
NBER's own `taxsim35` command for Stata is separate software. It sends every record to TAXSIM-35, either a local `taxsim35.exe` or NBER's servers. The copy NBER served on 2026-10-09 contains no reference to PolicyEngine, so it does not switch engines by year. The year switch exists only in this package, in the interfaces marked above. See the [design document](design.md#routing-by-year) for how the switch works.
@@ -243,7 +244,7 @@ These stop the whole run with an error naming the problem. Each one is covered b
| Input | Error |
|---|---|
| No `year` column | `Input data must contain a 'year' column` |
-| A blank `year` | `Cannot convert non-finite values (NA or inf) to integer` from the command line and `StitchedRunner`; `cannot convert float NaN to integer` from `PolicyEngineRunner` |
+| A blank `year` | `Cannot convert non-finite values (NA or inf) to integer` from the command line and `StitchedRunner`; `cannot convert float NaN to integer` from `PolicyEngineRunner`; `` `year` is missing or not a number in 1 record(s) `` from the R package |
| `state` outside 0 to 51, or not a whole number | `Record 1: 52 is not a valid TAXSIM SOI state code (0-51)` |
| `state` of -1 (TAXSIM's every-state option) | `TAXSIM state -1 (compute every state) is not supported` |
| `state` given as text, such as `CA` | `TAXSIM state code must be a number, got 'CA'` |
@@ -300,7 +301,7 @@ Choose the output with `idtl`: 0 for the standard columns, 2 for full detail, 5
Things to know when you use the results:
-- **Merge on `taxsimid`, not row order.** The command line and `StitchedRunner` return records in input order. `PolicyEngineRunner` and the R package currently return them grouped by year.
+- **Merge on `taxsimid`, not row order.** The command line, `StitchedRunner` and the R package return records in input order. `PolicyEngineRunner` currently returns them grouped by year.
- **Round to cents.** PolicyEngine computes in 32-bit floating point. A value can come back as `4009.199951171875`; it means 4009.20.
- **A file mixing years before and after 2021 has both engines' columns.** Cells an engine doesn't produce are blank, and TAXSIM-35 adds a column named for its build stamp, such as `cd2026081819`.
- **Results depend on the versions installed.** Record the policyengine-taxsim and policyengine-us versions with your results. The [design document](design.md#versions-and-pinning) explains how to pin them.
@@ -502,7 +503,7 @@ This script has not been run in Stata; the build machine has no Stata license. I
### R
-Uses base R and the command line, so it needs no R packages and works offline once policyengine-taxsim is installed. The `policyenginetaxsim` R package is an alternative, but it installs Python packages from GitHub on first use and computes every year with PolicyEngine.
+Uses base R and the command line, so it needs no R packages and works offline once policyengine-taxsim is installed. The `policyenginetaxsim` R package is an alternative that returns the same results: it switches engines by year as the command line does. On first use it creates a Python environment and installs a released policyengine-taxsim from PyPI, or, with `setup_policyengine(wheel_dir = ...)`, from a folder of wheels with no network access.
<!-- BEGIN GENERATED: example-r -->
```r
diff --git a/docs/security-and-deployment.md b/docs/security-and-deployment.md
index e0857411..c206ab77 100644
--- a/docs/security-and-deployment.md
+++ b/docs/security-and-deployment.md
@@ -51,7 +51,7 @@ Several installed packages can make network requests (requests, urllib3, httpx,
| What | When | Where it connects | What it sends |
|---|---|---|---|
| Installing with pip or uv | Install and upgrade | PyPI (or your configured mirror); GitHub for `git+https://` installs | Package requests |
-| R package setup, `setup_policyengine()` | Run once by you, or automatically by the first `policyengine_calculate_taxes()` when no environment exists | GitHub and PyPI, to install `policyengine-taxsim` from GitHub's `main` branch with `--upgrade`; may download Miniconda through reticulate | Package requests |
+| R package setup, `setup_policyengine()` | Run once by you, or automatically by the first `policyengine_calculate_taxes()` when no environment exists | PyPI (or your configured mirror), to install the policyengine-taxsim release that matches the R package; also downloads Miniconda through reticulate when no compatible Python is installed. With `wheel_dir`, nothing: it installs from that folder (`--no-index --find-links`) and stops instead of downloading Miniconda | Package requests |
| `StitchedRunner(..., use_remote_taxsim=True)` or `RemoteTaxsimRunner` | Only when Python code asks for it; no command-line flag sets it | NBER, `https://taxsim.nber.org/taxsim35/redirect.cgi` | Records before 2021, all TAXSIM input columns |
| Local API server, `uvicorn policyengine_taxsim.api:local_app` | Only if you start it; needs the `api` extra and the `modal` package | NBER for records before 2021; Mailchimp and Resend if its email endpoint is used | As above, plus the email address and the results |
| Web runner at policyengine.org/us/taxsim/run | When you upload a file in a browser | PolicyEngine's API on Modal (`policyengine--policyengine-taxsim-taxsimapi-serve.modal.run`) | The whole uploaded CSV |
@@ -333,6 +333,8 @@ Check the installation. With the snapshot's policyengine-us (2.38.1) this prints
printf 'taxsimid,year,state,mstat,page,pwages\n1,2023,33,1,30,52000\n' | taxsim-env/bin/policyengine-taxsim+The R package installs from the same folder, with no network access: Updates and patching
|
Main now routes nonprop into PolicyEngine (#1245): the generator reads sign- and state-dependent mappings, and the guides and tests drop the 'ignored' claim. From the re-review: a pre-2021 state -1 record replaces the records after it with its rows for other states; tested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… can't describe From the third review: negative nonprop arrives as a positive deduction, not as given. The generator now probes state-specific mappings before calling a column unused, raises on a positive amount arriving negative, and compares the union of inputs, raising on mismatched shapes. The generated tables are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Why
At the 2026-10-08 BLS Consumer Expenditure seminar, agency staff asked for three things the user guide doesn't give them:
A Stata user also read the slides as saying NBER's
taxsim35command switches engines by year.What this adds
Three guides under
docs/, linked from the README:Tables generated from the code
scripts/generate_docs_reference.pyrebuilds every block between<!-- BEGIN GENERATED -->markers:PolicyEngineRunner, thenTaxsimMicrosimDataset.generate) on probe records and reading the PolicyEngine inputs it builds, and from the input fileTaxsimRunnerwrites for the TAXSIM-35 executable. Nothing is simulated.docs/reference/input_variables.yaml.variable_mappings.yaml; executables from hashing and inspectingresources/; Python versions and dependencies frompyproject.toml,ci.ymland a hasheduv pip compilesnapshot indocs/dependencies/.Tests
tests/test_docs_reference.pypyproject.tomltests/test_input_guide_claims.pytests/test_worked_example.py.dtahand-off gives different taxes; the R script builds a different input or merges differentlytests/test_no_network.pypolicyenginesubcommand attempts a connection leaving the machine, a DNS lookup or an HTTP request; or leaves a temporary file behind (reported, not failed, on Windows)They add about five minutes to a test job on my machine.
Findings on the two claims made to BLS IT
"Nothing's going to our servers": true for a local install, false for the web runner.
sandbox-exec: completed, same output.use_remote_taxsim=True, and the local API server). No command-line flag reaches them."Pretty basic Python libraries, like NumPy": not accurate. An install pulls in 89 packages on Python 3.11 to 3.14 (85 on 3.10). They include pytest, IPython, plotly, pyvis, requests, httpx, huggingface-hub and a Census API client, mostly through policyengine-core and spm-calculator. A run never calls the network-capable ones, but an IT review will see all of them. policyengine-us and policyengine-core are AGPL-3.0; policyengine-taxsim is MIT.
Both are queued for Max as decision d1192. Whether the upload page should carry a data notice is d1193.
Routing by year, confirmed against the code
StitchedRunner.PE_MIN_YEAR = 2021: 2021 and later go to PolicyEngine, earlier years to the bundled TAXSIM-35 executable.policyenginesubcommand,StitchedRunnerand the web runner route.PolicyEngineRunnerand the R package compute every year with PolicyEngine, including years before 2021.taxsim35.ado, as served on 2026-10-09, has no reference to PolicyEngine. It runs TAXSIM for every year.Other things the audit turned up
Documented in the guides, not fixed here.
Inputs:
mstat8 and any code other than 1, 2 and 6 are computed as an ordinary unmarried filer (Support TAXSIM mstat 8 (dependent taxpayer) #1218).pbusincdiffers from the bundled TAXSIM-35 build. The emulator applies self-employment tax; build cd2026081819 doesn't. For a 2023 single filer with $100,000,fiitaxis 9,226.50 against 10,469.90.Records before 2021 (the bundled TAXSIM-35 path):
state-1 returns wrong answers. TAXSIM-35 returns a row per state andStitchedRunnerkeeps only as many rows as it sent, so the record comes back as Alabama and the pre-2021 records after it are replaced by its rows for other states. The state checks apply only to records from 2021 on.idtl5 doesn't work: one record returns only the standard columns, two or more raise anIndexError.chmodfails on installs owned by another account. Add --version and an opt-in --provenance sidecar to the CLI #1412 fixes both; the guides point to it and carry a marker to remove the note when it merges.siitax(7,428.45 against a state-only 4,417.25 on $100,000 in 2024) and returns 0 for both marginal rates.Deployment:
libgfortran5. The macOS build is x86-64 only (Rosetta on Apple silicon). None is code-signed.pip-auditfound nothing in the 3.11 to 3.14 snapshots.tficaon PolicyEngine rows includes employee state payroll taxes, whichficaand TAXSIM-35'stficadon't.uv.lockis stale; Remove the stale uv.lock and git-ignore it #1420 removes it.Since this PR opened, #1245 merged and routes
nonpropinto PolicyEngine; the branch merges main, and the generated tables and prose now describe the new sign-dependent mapping.Fixed here: the R package's documentation examples labeled SOI code 6 as California and 36 as New York. They are Colorado and Ohio; the examples now use 5 and 33.
Invariants
No tax calculation changes. The invariants the new tests hold:
nonpropadds nothing..dtahand-off through the command line, and the R script give the same taxes.Review
Independent GPT-6.1 Sol reviews, saved in
~/reviews/taxsim-agency-docs-1419/:idtl5, Maryland, glibc,tfica, temporary files, example identifiers, Stata validation, test coverage wording, zero marginal rates).Not verified
.dtainput, and the guide says the script itself is unrun.pip downloadcommands in the offline recipe were not run; a local hook blockspip. Their--platformflags were checked by computing the tags pip accepts withpackaging.tagsfor all five snapshots (the first draft's single flag would have rejected six packages). The install step was run withuv pip install --no-index --find-links ... --require-hasheswith all network access denied.axiom: n/a: documentation, tests and tooling; no policy rule changes.
🤖 Generated with Claude Code