Skip to content
Open
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
5 changes: 5 additions & 0 deletions changelog_entry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- bump: minor
changes:
added:
- Add Streamlined EITC reform with filing status dimension for max credit
- Add CTC Linear Phase-Out reform for complete phase-out between thresholds
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
description: The CTC fully phases out at this income, by filing status.

SINGLE:
2020-01-01: 240_000
JOINT:
2020-01-01: 440_000
SEPARATE:
2020-01-01: 175_000
HEAD_OF_HOUSEHOLD:
2020-01-01: 240_000
SURVIVING_SPOUSE:
2020-01-01: 440_000

metadata:
unit: currency-USD
period: year
label: CTC full phase-out income
breakdown:
- filing_status
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
description: The CTC linear phase-out reform is in effect if this is true.

values:
2020-01-01: false

metadata:
unit: bool
period: year
label: CTC linear phase-out in effect
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
description: The streamlined EITC reform is in effect if this is true.

values:
2020-01-01: false

metadata:
unit: bool
period: year
label: Streamlined EITC in effect
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
description: The EITC is capped at this amount for joint filers, based on number of children.

brackets:
- threshold:
2020-01-01: 0
amount:
2020-01-01: 0
- threshold:
2020-01-01: 1
amount:
2020-01-01: 4_993

metadata:
type: single_amount
period: year
threshold_unit: child
amount_unit: currency-USD
label: Streamlined EITC maximum (joint filers)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
description: The EITC is capped at this amount for single and head of household filers, based on number of children.

brackets:
- threshold:
2020-01-01: 0
amount:
2020-01-01: 0
- threshold:
2020-01-01: 1
amount:
2020-01-01: 3_995

metadata:
type: single_amount
threshold_unit: child
amount_unit: currency-USD
label: Streamlined EITC maximum (single/HOH filers)
period: year
3 changes: 3 additions & 0 deletions policyengine_us/reforms/ctc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
from .ctc_minimum_refundable_amount import (
create_ctc_minimum_refundable_amount_reform,
)
from .ctc_linear_phase_out import (
create_ctc_linear_phase_out_reform,
)
88 changes: 88 additions & 0 deletions policyengine_us/reforms/ctc/ctc_linear_phase_out.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from policyengine_us.model_api import *
from policyengine_core.periods import period as period_


def create_ctc_linear_phase_out() -> Reform:
"""
CTC Linear Phase-Out Reform:
- Replaces the standard CTC phase-out with a linear phase-out
- Uses existing IRS threshold for phase-out start
- Adds new parameter for phase-out end threshold by filing status

To model full Enhanced CTC policy:
- Use this reform for linear phase-out
- Use ctc_minimum_refundable_amount reform for minimum refundability
- Set existing IRS parameters for credit amounts and phase-in rates

This reform adds:
- gov.contrib.ctc.linear_phase_out.in_effect (reform switch)
- gov.contrib.ctc.linear_phase_out.end (full phase-out by filing status)
"""

class ctc_phase_out(Variable):
value_type = float
entity = TaxUnit
label = "CTC reduction from income"
unit = USD
documentation = (
"Reduction of the total CTC due to income with linear phase-out."
)
definition_period = YEAR

def formula(tax_unit, period, parameters):
# Linear phase-out between threshold and end
p = parameters(period).gov.contrib.ctc.linear_phase_out
ctc_irs = parameters(period).gov.irs.credits.ctc
filing_status = tax_unit("filing_status", period)
income = tax_unit("adjusted_gross_income", period)

# Use existing IRS threshold for phase-out start
phase_out_threshold = ctc_irs.phase_out.threshold[filing_status]
# Use new parameter for phase-out end
phase_out_end = p.end[filing_status]

# Calculate the maximum CTC for the tax unit
ctc_maximum = tax_unit("ctc_maximum_with_arpa_addition", period)

# Calculate phase-out as linear reduction
phase_out_range = max_(1, phase_out_end - phase_out_threshold)
excess_income = max_(0, income - phase_out_threshold)

# Phase-out rate = ctc_maximum / phase_out_range
phase_out_rate = ctc_maximum / phase_out_range
reduction = excess_income * phase_out_rate
return min_(reduction, ctc_maximum)

class reform(Reform):
def apply(self):
self.update_variable(ctc_phase_out)

return reform


def create_ctc_linear_phase_out_reform(
parameters, period, bypass: bool = False
):
if bypass:
return create_ctc_linear_phase_out()

p = parameters.gov.contrib.ctc.linear_phase_out

reform_active = False
current_period = period_(period)

for i in range(5):
if p(current_period).in_effect:
reform_active = True
break
current_period = current_period.offset(1, "year")

if reform_active:
return create_ctc_linear_phase_out()
else:
return None


ctc_linear_phase_out = create_ctc_linear_phase_out_reform(
None, None, bypass=True
)
3 changes: 3 additions & 0 deletions policyengine_us/reforms/eitc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from .halve_joint_eitc_phase_out_rate import (
create_halve_joint_eitc_phase_out_rate_reform,
)
from .streamlined_eitc import (
create_streamlined_eitc_reform,
)
69 changes: 69 additions & 0 deletions policyengine_us/reforms/eitc/streamlined_eitc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from policyengine_us.model_api import *
from policyengine_core.periods import period as period_


def create_streamlined_eitc() -> Reform:
"""
Streamlined EITC Reform:
- Single schedule for all filers with dependent children (1+ children)
- Maximum credit varies by filing status: single vs married

To model the full policy, set these existing IRS parameters:
- gov.irs.credits.eitc.phase_in_rate
- gov.irs.credits.eitc.phase_out.start (phase-out start for single)
- gov.irs.credits.eitc.phase_out.joint_bonus (additional amount for married)
- gov.irs.credits.eitc.phase_out.rate (phase-out rate)

This reform adds:
- gov.contrib.streamlined_eitc.max.single (max credit for single/HOH)
- gov.contrib.streamlined_eitc.max.joint (max credit for married filing jointly)
"""

class eitc_maximum(Variable):
value_type = float
entity = TaxUnit
label = "Maximum EITC"
definition_period = YEAR
reference = "https://www.law.cornell.edu/uscode/text/26/32#a"
unit = USD

def formula(tax_unit, period, parameters):
child_count = tax_unit("eitc_child_count", period)
p = parameters(period).gov.contrib.streamlined_eitc
filing_status = tax_unit("filing_status", period)
joint = filing_status == filing_status.possible_values.JOINT
return where(
joint,
p.max.joint.calc(child_count),
p.max.single.calc(child_count),
)

class reform(Reform):
def apply(self):
self.update_variable(eitc_maximum)

return reform


def create_streamlined_eitc_reform(parameters, period, bypass: bool = False):
if bypass:
return create_streamlined_eitc()

p = parameters.gov.contrib.streamlined_eitc

reform_active = False
current_period = period_(period)

for i in range(5):
if p(current_period).in_effect:
reform_active = True
break
current_period = current_period.offset(1, "year")

if reform_active:
return create_streamlined_eitc()
else:
return None


streamlined_eitc = create_streamlined_eitc_reform(None, None, bypass=True)
8 changes: 8 additions & 0 deletions policyengine_us/reforms/reforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
)
from .biden.budget_2025 import create_capital_gains_tax_increase_reform
from .eitc import create_halve_joint_eitc_phase_out_rate_reform
from .eitc import create_streamlined_eitc_reform
from .states.ny.wftc import create_ny_working_families_tax_credit_reform
from .harris.lift.middle_class_tax_credit import (
create_middle_class_tax_credit_reform,
Expand Down Expand Up @@ -65,6 +66,7 @@
create_ctc_per_child_phase_in_reform,
create_ctc_per_child_phase_out_reform,
create_ctc_minimum_refundable_amount_reform,
create_ctc_linear_phase_out_reform,
)
from .snap import (
create_abolish_snap_deductions_reform,
Expand Down Expand Up @@ -298,6 +300,10 @@ def create_structural_reforms_from_parameters(parameters, period):
aca_ptc_700_fpl_cliff = create_aca_ptc_700_fpl_cliff_reform(
parameters, period
)
streamlined_eitc = create_streamlined_eitc_reform(parameters, period)
ctc_linear_phase_out = create_ctc_linear_phase_out_reform(
parameters, period
)

reforms = [
afa_reform,
Expand Down Expand Up @@ -358,6 +364,8 @@ def create_structural_reforms_from_parameters(parameters, period):
aca_ptc_additional_bracket,
aca_ptc_simplified_bracket,
aca_ptc_700_fpl_cliff,
streamlined_eitc,
ctc_linear_phase_out,
]
reforms = tuple(filter(lambda x: x is not None, reforms))

Expand Down
Loading