Skip to content

Fix MT CTC reform entity level issue#7750

Merged
DTrim99 merged 6 commits intoPolicyEngine:mainfrom
DTrim99:fix-mt-ctc-entity-level
Mar 10, 2026
Merged

Fix MT CTC reform entity level issue#7750
DTrim99 merged 6 commits intoPolicyEngine:mainfrom
DTrim99:fix-mt-ctc-entity-level

Conversation

@DTrim99
Copy link
Collaborator

@DTrim99 DTrim99 commented Mar 10, 2026

Summary

  • Fix mt_ctc and mt_hb268 entity level from TaxUnit to Person to prevent cost inflation
  • Montana's refundable credits calculation is at Person level, causing TaxUnit credits to be broadcast to each person (~4.57x inflation)
  • Assign credit only to head of tax unit to avoid duplication

Evidence

Before fix:

MT CTC credit total: $239.31M
Household income change: $1,093.52M  
Ratio: 4.57x (WRONG)

After fix:

MT CTC credit total: $239.31M
Household income change: $239.28M
Ratio: 1.00x (CORRECT)

Test plan

  • Verify mt_ctc entity is now Person
  • Verify mt_hb268 entity is now Person
  • Verify ratio of credit to income change is 1.00x
  • Import test passes

Fixes #7749

🤖 Generated with Claude Code

@DTrim99
Copy link
Collaborator Author

DTrim99 commented Mar 10, 2026

Technical Details

Root Cause

The mt_refundable_credits_before_renter_credit variable is defined at Person entity level and uses adds = \"gov.states.mt.tax.income.credits.refundable\" to sum credits from a parameter list.

When mt_ctc was a TaxUnit variable added to this Person-level sum, PolicyEngine broadcast the tax unit value to each person in the household.

Example

A tax unit with 4 people receiving $2,699 credit:

  • Before: Each person got $2,699 → $10,796 total (4x inflation)
  • After: Only head gets $2,699 → $2,699 total (correct)

Fix Pattern

Follows the same pattern that should be used for all credits added to mt_refundable_credits:

entity = Person
...
is_head = person("is_tax_unit_head", period)
return is_head * eligible * credit

Related Files

  • policyengine_us/variables/gov/states/mt/tax/income/tax_calculation/mt_refundable_credits_before_renter_credit.py - The Person-level aggregator that caused the issue

@DTrim99 DTrim99 changed the title Fix MT CTC entity level causing 4.57x cost inflation Fix MT CTC reform entity level Mar 10, 2026
@DTrim99 DTrim99 changed the title Fix MT CTC reform entity level Fix MT CTC reform entity level issue Mar 10, 2026
@DTrim99 DTrim99 force-pushed the fix-mt-ctc-entity-level branch from 038e4f7 to 8a82fc8 Compare March 10, 2026 17:54
@DTrim99
Copy link
Collaborator Author

DTrim99 commented Mar 10, 2026

Updated to fix both files after rebasing from upstream/main:

  1. policyengine_us/reforms/states/mt/ctc/mt_ctc.py
  2. policyengine_us/reforms/states/mt/hb268/mt_hb268.py

Both now use entity = Person and assign credit only to head of tax unit.

Verified fix:

MT CTC credit total: $239.31M
Household income change: $239.28M
Ratio: 1.00x ✓

Change mt_ctc and mt_hb268 from TaxUnit to Person entity to fix cost inflation.

Montana's refundable credits calculation (mt_refundable_credits_before_renter_credit)
is at Person entity level. When TaxUnit variables were added via the `adds` directive,
PolicyEngine broadcast the tax unit value to each person, causing ~4.57x cost inflation.

Fix follows the pattern used in mt_newborn_credit:
- Change entity from TaxUnit to Person
- Assign credit only to head of tax unit to avoid duplication

Before fix: Ratio 4.57x (credit $239M, income change $1,093M)
After fix: Ratio 1.00x (credit $239M, income change $239M)

Also updates tests to expect person-level outputs where only head receives credit.

Fixes PolicyEngine#7749

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@DTrim99 DTrim99 force-pushed the fix-mt-ctc-entity-level branch from ac060e7 to bbc2086 Compare March 10, 2026 18:27
@DTrim99
Copy link
Collaborator Author

DTrim99 commented Mar 10, 2026

Updated tests to expect person-level outputs.

Since mt_ctc and mt_hb268 are now Person-level variables with credit only assigned to head, the test outputs changed from scalar values to arrays:

Before: mt_ctc: 1800
After: mt_ctc: [1800, 0] (head gets 1800, child gets 0)

All 16 previously failing tests should now pass.

Add households sections with state_code: MT to tests that were
missing them. Required because defined_for = StateCode.MT needs
the state code in the household entity.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@DTrim99
Copy link
Collaborator Author

DTrim99 commented Mar 10, 2026

Updated tests to include households section with state_code: MT for all tests that were missing it. This is required because the reforms now use defined_for = StateCode.MT which needs the state code in the household entity.

@DTrim99 DTrim99 requested a review from PavelMakarchuk March 10, 2026 18:38
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@DTrim99
Copy link
Collaborator Author

DTrim99 commented Mar 10, 2026

PR Review: Fix MT CTC Reform Entity Level Issue

🟢 Overall Assessment: APPROVE

This PR correctly fixes a microsimulation cost inflation issue by changing the entity level from TaxUnit to Person and assigning credits only to the tax unit head. The approach follows established patterns used by other Montana credits (MT EITC, MT newborn credit).


🔴 Critical Issues (1)

1. HB268 Missing Qualifying Child Filter

  • File: policyengine_us/reforms/states/mt/hb268/mt_hb268.py:19-20
  • Issue: Credit is calculated for ALL persons based on age, not just qualifying children
  • Current:
    age = person("age", period)
    credit_amount = person.tax_unit.sum(p.amount.calc(age))
  • Should be:
    age = person("age", period)
    is_qualifying = person("ctc_qualifying_child", period)
    credit_amount = person.tax_unit.sum(p.amount.calc(age) * is_qualifying)
  • Impact: Without this filter, non-qualifying dependents could contribute to credit amounts. However, since ages 6+ return $0 in the parameter, this may not cause practical issues in most cases.

🟡 Should Address (4)

1. Inconsistent Rounding Method

  • mt_ctc.py uses np.ceil() (ceiling - any fraction triggers reduction)
  • mt_hb268.py uses // (floor division - only full increments count)
  • Action: Verify HB268 Section 1(6) specifies floor division, or align with ceiling if required by law

2. MT CTC Variables Missing References

  • mt_ctc and mt_ctc_eligible variables have no reference attribute
  • As a contrib/hypothetical reform, consider adding: reference = "PolicyEngine Montana Child Tax Credit Reform (hypothetical)"

3. MT CTC Parameters Missing References

  • 6 parameter files in gov/contrib/states/mt/ctc/ lack reference metadata
  • These are hypothetical policy parameters, so external references don't apply, but documentation references would improve maintainability

4. HB268 Variable References Missing Page Numbers

  • Current: reference = "https://leg.mt.gov/bills/2023/billpdf/HB0268.pdf"
  • Better: Add #page=X anchors for direct navigation

🟢 Suggestions (Test Coverage)

Missing Test Scenarios:

  1. Filing status coverage: No tests for HEAD_OF_HOUSEHOLD, SEPARATE, or SURVIVING_SPOUSE (MT CTC)
  2. Age boundary tests: Age 0 (newborn), 5, 6, 17, 18 transitions not explicitly tested
  3. Phase-out to zero: No test verifying credit completely phases out at high income
  4. HB268 integer division edge case: AGI $50,500 should show no reduction (0 increments)
  5. Explicit spouse exclusion: Add test with married couple explicitly verifying spouse gets $0

Validation Summary

Check Result
Regulatory Accuracy ✅ Correct pattern (follows MT EITC, newborn credit)
Reference Quality ⚠️ HB268 has references; MT CTC missing
Code Patterns ✅ Mostly correct; one filter missing in HB268
Test Coverage ⚠️ Good but has gaps (filing statuses, boundaries)
CI Status ✅ All checks passing

What's Correct

✅ Entity change from TaxUnit to Person - follows established patterns
is_head * eligible * credit pattern - correctly assigns to head only
person.tax_unit() access pattern for TaxUnit variables
max_() vectorized function usage
✅ No hard-coded values in formulas
✅ Test outputs updated to arrays [credit, 0, 0]
✅ Tests include state_code: MT in households block


Next Steps

To auto-fix issues: /fix-pr 7750

Or address manually:

  1. Add is_qualifying filter to HB268 credit calculation
  2. Verify HB268 rounding method against legislation
  3. Consider adding reference attributes for documentation

🤖 Review generated by PolicyEngine validator agents

DTrim99 and others added 3 commits March 10, 2026 15:46
- Add ctc_qualifying_child filter to HB268 credit calculation to prevent
  non-qualifying dependents from contributing to credit amounts
- Change HB268 from floor division (//) to np.ceil() for consistent
  ceiling behavior (any fraction triggers reduction)
- Add MT CTC filing status tests: HEAD_OF_HOUSEHOLD, SEPARATE, SURVIVING_SPOUSE
- Add MT CTC age boundary tests: 0, 5, 6, 17
- Add MT HB268 age boundary tests: 0, 5, 6
- Add MT HB268 ceiling reduction test for $50,500 AGI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Federal CTC qualifying child must be under age 17, so age 16 is the
upper boundary for qualifying children, not age 17.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The MT CTC reform covers children aged 0-17, but ctc_qualifying_child
only includes ages 0-16. This extends eligibility to also include
17-year-old dependents who meet other requirements.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@DTrim99 DTrim99 merged commit 6d5e971 into PolicyEngine:main Mar 10, 2026
7 checks passed
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.

Fix MT CTC and HB268 entity level causing 4.57x cost inflation

1 participant