Limit the CTC by actual tax liability and make formula branches calculate under their overrides - #9741
Open
MaxGhenis wants to merge 1 commit into
Open
Limit the CTC by actual tax liability and make formula branches calculate under their overrides#9741MaxGhenis wants to merge 1 commit into
MaxGhenis wants to merge 1 commit into
Conversation
…tual liability A policyengine-core branch starts as a copy of every array its parent has cached, and set_input on it clears none of them, so a value the parent calculated from the overridden input answers for the branch. - ctc_limiting_tax_liability no longer recomputes the liability without SALT in a "no_salt" branch. The branch usually inherited the liability with SALT, so the CTC limit depended on which variable was calculated first. 26 U.S.C. 26(a) limits the credit by the actual tax liability, which reflects the SALT deduction. - get_override_branch (tools/override_branch.py) creates a comparison branch once per period and, when the parent has already calculated an overridden input, drops the arrays the branch copied except inputs. The itemization, Delaware and Virginia EITC refundability, Idaho aged or disabled, Missouri TANF caretaker and Medicaid SSI-supplement branches use it; the Alabama 2020-IRC and New York pinned-parameter branches are created per period. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #9741 +/- ##
============================================
- Coverage 100.00% 64.11% -35.89%
============================================
Files 4 16 +12
Lines 76 301 +225
Branches 2 15 +13
============================================
+ Hits 76 193 +117
- Misses 0 106 +106
- Partials 0 2 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Formula branches copy every array their parent has cached when they are created, and
set_inputon a branch clears none of them (policyengine-coreSimulation.clone→InMemoryStorage.clone;Holder.get_arrayfalls back to the copieddefaultkey). A value the parent calculated from the overridden input therefore answers for the branch.ctc_limiting_tax_liabilityrecomputed the liability without SALT in ano_saltbranch. Whether that branch saw its override depended on which variable the caller asked for first: in policyengine.py's US output order it applied the no-SALT liability at the top level; asking forincome_taxfirst gave the liability with SALT; inside the itemizing branch it always gave the liability with SALT. The law limits the credit by the actual liability: 26 U.S.C. 26(a) caps subpart A credits at regular tax liability (the chapter 1 tax, on taxable income after itemized deductions) plus the section 55(a) tax; 24(b)(3) was struck in 2013; Schedule 8812 Credit Limit Worksheet A line 1 is Form 1040 line 18. The variable now readsincome_tax_before_creditsdirectly. The cycle the branch once avoided is gone:salt_deductionreadsstate_withheld_income_tax, per-person withholding estimates that do not read federal tax or credits.get_override_branch(policyengine_us/tools/override_branch.py) keeps one branch per name and period, creates it again for another period or other override values, and, when the parent already has a value for an overridden input at branch creation, drops every copied array except inputs. Otherwise it shares the parent's cache: no cached value can have been calculated from an input value that did not yet exist, which is the usual case because the formula branches while the parent is still calculating the overridden variable. Used by itemizing/not itemizing, Delaware and Virginia EITC refundability, Idaho aged or disabled credit/deduction, Missouri TANF non-parent caretaker and Medicaid for SSI state supplements. The Alabama 2020-IRC and New York pre-ARPA EITC / pre-TCJA CTC branches (pinned parameters, own variable deletion) are created per period withget_branch_for_period.This supersedes the per-period branch helper in #9738; that PR can keep its multi-year test and core bump.
Evidence
Audit of every formula branch (
git grep get_branch policyengine_us/variables) on 27 household cases: each read a branch served from an inherited key was compared with a fresh simulation that sets the branch's inputs before calculating anything. On main, only theno_saltoverride is shadowed in single-year runs (always when nested in another branch; at top level depending on order); the itemization and state-choice branches are shadowed when the parent already has the input (e.g.tax_unit_itemizesgiven as input) and in year 2 of a multi-year simulation; MTR, capital-gains MTR, trust-fund-revenue and employer-payroll branches clear their arrays and are effective. Example, CA couple 2025:ctc_limiting_tax_liabilityis $43,038.46 when household net income is calculated first and $35,670.97 when income tax is; it is $35,670.97 in every order with this PR.Impact (Enhanced CPS 2024 file, 2026, full sample, real runs)
Real runs only; scripts, logs and arrays are in the investigation folder (
microsim_run.py,diff_ms.py,ms/). Dataset: the Enhanced CPS 2024 h5 used for the 2026-10-01 multi-year investigation (sha256 faffaf68…), SPM thresholds in national mode; the two SPM poverty flags are left out because the SPM calculator rejects some units in that file.Invariants and tests
policyengine_us/tests/core/test_override_branches.py, on a seeded sample of 48 households across 10 states (itemizers, SALT, children, aged dependents):ctc_limiting_tax_liability= max(0, income tax before credits − other non-refundable credits); non-refundable + refundable CTC = CTC; the sample has itemizers with SALT whose limit binds.get_override_branchreuse/recreation anddrop_inherited_valueskeep inputs (including ancestor-branch inputs) and drop calculated values.On main, 6 of these fail (1, three orders in 2, both cases in 4).
axiom: us:statutes/26/26 encoded-correct (caps non-refundable credits by income_tax_before_credits; us/statutes/26/26.test.yaml
nonrefundable_credits_capped_by_tax_before_credits)🤖 Generated with Claude Code