Skip to content

R: compute pre-2021 records with TAXSIM-35 like the command line, and install a pinned release - #1422

Open
MaxGhenis wants to merge 5 commits into
mainfrom
r-stitched-runner-pinned-install
Open

MaxGhenis wants to merge 5 commits into
mainfrom
r-stitched-runner-pinned-install

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

The R package now gives the same results as the command line. policyengine_calculate_taxes() sends tax years before 2021 to the bundled TAXSIM-35, as policyengine-taxsim does, and setup_policyengine() installs one released policyengine-taxsim from PyPI instead of GitHub's main with --upgrade, with a way to install from a folder of wheels on machines without internet access.

This changes results for R users with pre-2021 records. Those records came from PolicyEngine, which this project compares with TAXSIM-35 only from 2021 on; they now come from TAXSIM-35. Results for 2021 and later are unchanged (tested below). engine = "policyengine" keeps the old behavior. The changelog fragment is changed and says so.

Routing by year

  • policyengine_calculate_taxes() calls StitchedRunner (was PolicyEngineRunner). New argument engine = c("auto", "policyengine"): "policyengine" passes pe_min_year = 0, so every record goes to PolicyEngine and rows still come back in input order.
  • The progress message says how many records each engine computes, for example Calculating taxes: 2 records with PolicyEngine (tax years from 2021 on), 1 with TAXSIM-35 (earlier years).... The cutoff is read from StitchedRunner.PE_MIN_YEAR.
  • Rows come back in input order (they were grouped by year). Mixed-year results have both engines' columns, NA where an engine produces none, plus TAXSIM-35's build column.
  • A missing or non-numeric year now stops with "Every record needs a tax year", since the year picks the engine.
  • An installed policyengine-taxsim without StitchedRunner (before March 2026) gets an error saying to run setup_policyengine(force = TRUE).
  • idtl is still set from return_all_information (0 or 2); the docs now say an idtl column in the data is ignored.

Setup

  • Default: pip install policyengine-taxsim==<pin> from PyPI, one pip call, no --upgrade. <pin> is .PE_TAXSIM_VERSION in R/setup.R, currently 3.1.1.
  • policyengine_taxsim_version and policyengine_us_version pin each package; both go into the same pip call, so pip resolves them together (before, policyengine-us was pinned in a second call after an upgrading install).
  • wheel_dir: installs only from that folder (--no-index --find-links <dir>). Without a version, the folder decides which policyengine-taxsim. The folder is checked for a matching policyengine_taxsim-*.whl before anything is removed. If no compatible Python is found, the function stops instead of downloading Miniconda.
  • python: build the environment from a given interpreter, skipping reticulate's search. Useful when wheels fit one Python version, or a machine has 3.10 and 3.11.
  • The environment is created with virtualenv_create(..., packages = FALSE). By default reticulate upgrades pip, wheel and setuptools and installs numpy from the index at that step, which would defeat wheel_dir (reticulate 1.47.0 R/virtualenv.R). pip is run directly because reticulate::virtualenv_install() always adds --upgrade (R/pip.R, pip_install()).
  • Bad arguments (a version like "latest", a missing folder, a folder without the wheel) stop before force = TRUE removes an existing environment.
  • Setup ends by printing the installed versions and the setup_policyengine(force = TRUE, policyengine_taxsim_version = ..., policyengine_us_version = ...) call that reinstalls them. Asking for pins when an environment already exists says nothing was installed.
  • R package version 0.1.0 → 0.2.0.

Keeping the pin current

.github/bump_version.py, which the release job runs, now rewrites .PE_TAXSIM_VERSION together with pyproject.toml and __init__.py, and exits non-zero if it can't find exactly one such line. So the R package on any commit of main pins the release made from that commit (or, between the merge and the version commit, the previous release, which also has StitchedRunner). tests/test_r_package.py fails if the pin and pyproject.toml differ. Every release from 2.10.0 on is on PyPI, so the pin has been installable for the whole period this applies to.

How this relates to other open PRs

Invariants and how they are tested

  1. Each record goes to exactly one engine: PolicyEngine gets exactly the years ≥ PE_MIN_YEAR, TAXSIM-35 the rest, nothing dropped or duplicated. Hypothesis property test on StitchedRunner (tests/test_r_package.py), and a 200-case randomized test of the R counts, including that a later cutoff never sends more records to PolicyEngine.
  2. pe_min_year = 0 sends every record to PolicyEngine, for any years (Hypothesis).
  3. Differential, R vs command line: for the same records policyengine_calculate_taxes() equals policyengine-taxsim policyengine on the same CSV, for idtl 0 and 2 (R integration test; mixed 2015–2023 records, single and joint, with dependents).
  4. Differential, old behavior preserved: engine = "policyengine" equals PolicyEngineRunner on the same records, in R (integration test) and in Python (StitchedRunner(pe_min_year=0) vs PolicyEngineRunner).
  5. 2021+ unchanged: default and engine = "policyengine" agree on every record from 2021 on, and TAXSIM-35's build column is filled exactly for the pre-2021 records.
  6. Rows in input order for both engines (R integration test).
  7. No pip call from setup upgrades or installs from GitHub; --no-index/--find-links appear exactly when wheel_dir is given: exhaustive over taxsim version × us version × folder (R unit test). With wheel_dir, no Miniconda download (unit test) and no network at all (CI, below).
  8. Release pin round-trips: for any version, update_r_pin makes the pin read back as that version, changes no other line, and is idempotent (Hypothesis); the full release step on each fragment type moves pyproject, __init__ and the R pin together (parametrized).

CI

  • r-package (from R wrapper and dashboard refresh: report the TAXSIM build and record provenance #1414): unit tests plus the integration tests against this checkout, with Python required.
  • New r-package-install job, which runs setup_policyengine() for real in fresh virtualenv roots: (1) the first policyengine_calculate_taxes() call installs the pinned release from PyPI and computes a 2019 and a 2023 record; (2) pip download the wheels (the command the README gives), then install from them and compute inside unshare --net with only loopback (.github/without-network.sh), with a test that fails if PyPI is reachable; (3) the same with both versions pinned.

Tests run locally

  • R testthat::test_local() with no Python environment: 1,259 expectations pass; 5 tests skip (the 2 integration tests and the 3 install tests).
  • R engine tests against a Python 3.11 environment (this checkout, policyengine-us 2.38.0, macOS, POLICYENGINETAXSIM_REQUIRE_PYTHON=true): 1,125 expectations pass, 0 skipped. This covers invariants 3 to 6 above.
  • pytest tests/test_r_package.py tests/test_stitched_runner.py: 29 passed (12 new).
  • ruff format --check .: clean.
  • Not run locally: test-install.R. The machine was under a hold on creating virtual environments, so these run only in CI's r-package-install job.

What the change does to one record (same environment): for a 2015 California single filer with $30,000 wages and one child, fiitax was −2,136.73 from PolicyEngine and is now −1,190.37 from TAXSIM-35 (build cd2026081819). A 2019 Texas joint filer gave 8,749 from both.

Not changed

axiom: n/a: R wrapper engine selection and installation; no policy change.

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits October 9, 2026 21:25
policyengine_calculate_taxes() now runs StitchedRunner, so tax years before
2021 are computed by the bundled TAXSIM-35 as on the command line, with
engine = "policyengine" to compute every year with PolicyEngine as before.
setup_policyengine() installs the policyengine-taxsim release matching the R
package from PyPI in one pip call, without --upgrade, and can pin both
packages, install from a folder of wheels with no network access, and build
the environment from a given Python.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
policyengine-taxsim Ready Ready Preview Nov 11, 2026 5:39am UTC

Request Review

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… main merge

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Sweep status (PR sweep of 2026-11-11): an independent Opus 5.5 review of ffe6986 found one blocker: the main merge left the R package's release pin at 3.1.1 while pyproject says 3.3.1, so test_r_package_installs_this_release_by_default failed and R would have installed a release older than the November 1 TAXSIM binaries. The sweep fixed the pin (7670ab0). A delta re-review approved 7670ab0 (30/30 of tests/test_r_package.py and tests/test_stitched_runner.py pass), and the PR is now marked ready. The test jobs still fail on test_or_kicker_output[2025], as every branch cut from current main does: since PolicyEngine/policyengine-us#10093, Oregon's federal-liability subtraction nets out the imputed assigned_aca_ptc. #1518 fixes it. Once #1518 merges, update this branch and merge on green.

This branch was successfully deployed

1 active deployment
Preview — 7670ab08 Deployed Nov 11, 2026 by vercel[bot]
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