diff --git a/changelog.d/fix-ar-reduction-uprating.fixed.md b/changelog.d/fix-ar-reduction-uprating.fixed.md new file mode 100644 index 00000000000..d2f966a8a12 --- /dev/null +++ b/changelog.d/fix-ar-reduction-uprating.fixed.md @@ -0,0 +1 @@ +Make Arkansas income tax projections for 2027 and later consistent. The model now derives the minus adjustments from Act 1 of 2026's indexed statutory tables: table (A), the high-income table (B), and the (C) bracket-adjustment rows. Before, the minus adjustments stayed at their 2026 values while the rate thresholds rose, so tax fell by as much as $111.98 as income crossed a 2026 bound. Rate-table bracket bounds now round to the nearest $100 instead of down, per A.C.A. § 26-51-201(d)(1); this also moves the bounds that projected Arkansas withholding reads. The $94,700 high-income threshold, the (B) 2% row top and each (C) row bound, which were not indexed before, are now indexed and rounded the same way. The (C) dollar amounts stay fixed. diff --git a/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/bracket_adjustment.yaml b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/bracket_adjustment.yaml new file mode 100644 index 00000000000..0a2268342f7 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/bracket_adjustment.yaml @@ -0,0 +1,349 @@ +description: Arkansas subtracts this bracket adjustment amount from the high-income tax table result, based on net taxable income. +metadata: + type: single_amount + threshold_unit: currency-USD + amount_unit: currency-USD + label: Arkansas high-income bracket adjustment + reference: + - title: A.C.A. § 26-51-201(a)(4)(C), as amended by Act 1 of 2026 (1st Extraordinary Session), § 1 + href: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2026S1%2FPublic%2FACT1.pdf#page=2 + - title: A.C.A. § 26-51-201(d)(1), as amended by Act 532 of 2023, § 2 (bracket amounts indexed annually, rounded to the nearest $100) + href: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2023R%2FPublic%2FACT532.pdf#page=4 +# Each threshold is the "Less Than or Equal To" bound of the row before, and +# the amount applies to net income ABOVE it (the formula calls calc with +# right=True): the statute's first row, $94,701-$94,800, is threshold 94,700. +# Keying rows on these round bounds lets each one be indexed and rounded to the +# nearest $100 like the high-income threshold, which the first row shares. +# The dollar amounts are not indexed: 26-51-201(d)(1) indexes only bracket +# bounds, and DFA's 2022-2025 cards kept each Act's $10 steps. +brackets: + - threshold: + values: + 2026-01-01: 94_700 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 290 + - threshold: + values: + 2026-01-01: 94_800 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 280 + - threshold: + values: + 2026-01-01: 94_900 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 270 + - threshold: + values: + 2026-01-01: 95_000 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 260 + - threshold: + values: + 2026-01-01: 95_100 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 250 + - threshold: + values: + 2026-01-01: 95_200 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 240 + - threshold: + values: + 2026-01-01: 95_300 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 230 + - threshold: + values: + 2026-01-01: 95_400 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 220 + - threshold: + values: + 2026-01-01: 95_500 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 210 + - threshold: + values: + 2026-01-01: 95_600 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 200 + - threshold: + values: + 2026-01-01: 95_700 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 190 + - threshold: + values: + 2026-01-01: 95_800 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 180 + - threshold: + values: + 2026-01-01: 95_900 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 170 + - threshold: + values: + 2026-01-01: 96_000 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 160 + - threshold: + values: + 2026-01-01: 96_100 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 150 + - threshold: + values: + 2026-01-01: 96_200 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 140 + - threshold: + values: + 2026-01-01: 96_300 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 130 + - threshold: + values: + 2026-01-01: 96_400 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 120 + - threshold: + values: + 2026-01-01: 96_500 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 110 + - threshold: + values: + 2026-01-01: 96_600 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 100 + - threshold: + values: + 2026-01-01: 96_700 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 90 + - threshold: + values: + 2026-01-01: 96_800 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 80 + - threshold: + values: + 2026-01-01: 96_900 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 70 + - threshold: + values: + 2026-01-01: 97_000 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 60 + - threshold: + values: + 2026-01-01: 97_100 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 50 + - threshold: + values: + 2026-01-01: 97_200 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 40 + - threshold: + values: + 2026-01-01: 97_300 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 30 + - threshold: + values: + 2026-01-01: 97_400 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 20 + - threshold: + values: + 2026-01-01: 97_500 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 10 + - threshold: + values: + 2026-01-01: 97_600 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + amount: + 2026-01-01: 0 diff --git a/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/rate.yaml b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/rate.yaml new file mode 100644 index 00000000000..643edacf349 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/rate.yaml @@ -0,0 +1,29 @@ +description: Arkansas applies these marginal rates to the entire net income of a filer whose net income exceeds the high-income threshold, before subtracting the bracket adjustment. +metadata: + type: marginal_rate + threshold_unit: currency-USD + rate_unit: /1 + label: Arkansas high-income tax table + reference: + - title: A.C.A. § 26-51-201(a)(4)(B), as amended by Act 1 of 2026 (1st Extraordinary Session), § 1 + href: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2026S1%2FPublic%2FACT1.pdf#page=2 + - title: A.C.A. § 26-51-201(d)(1), as amended by Act 532 of 2023, § 2 (bracket amounts indexed annually, rounded to the nearest $100) + href: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2023R%2FPublic%2FACT532.pdf#page=4 +brackets: + - threshold: + 2026-01-01: 0 + rate: + 2026-01-01: 0.02 + # The statute's rows are $0-$4,700 at 2% and $4,701 and above at 3.7%, so + # 3.7% applies to each dollar above $4,700. + - threshold: + values: + 2026-01-01: 4_700 + metadata: + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + rate: + 2026-01-01: 0.037 diff --git a/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/threshold.yaml b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/threshold.yaml new file mode 100644 index 00000000000..40ca4b1fd92 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/threshold.yaml @@ -0,0 +1,17 @@ +description: Arkansas taxes net income above this amount under the high-income tax table, less the bracket adjustment. +values: + 2026-01-01: 94_700 +metadata: + unit: currency-USD + period: year + label: Arkansas high-income tax table threshold + uprating: + parameter: gov.irs.uprating + rounding: + type: nearest + interval: 100 + reference: + - title: A.C.A. § 26-51-201(a)(4)(A)-(B), as amended by Act 1 of 2026 (1st Extraordinary Session), § 1 + href: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2026S1%2FPublic%2FACT1.pdf#page=1 + - title: A.C.A. § 26-51-201(d)(1), as amended by Act 532 of 2023, § 2 (bracket amounts indexed annually, rounded to the nearest $100) + href: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2023R%2FPublic%2FACT532.pdf#page=4 diff --git a/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/rate.yaml b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/rate.yaml index b30f635a6c0..7ab59a86034 100644 --- a/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/rate.yaml +++ b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/rate.yaml @@ -1,4 +1,4 @@ -description: Arkansas taxes personal income at these amounts. +description: Arkansas taxes personal income at these rates, by taxable income. Through 2025 the row's rate applies to all taxable income, less the published reduction; from 2026 each rate applies to the income within its row, up to the high-income threshold. metadata: type: single_amount threshold_unit: currency-USD @@ -50,6 +50,10 @@ metadata: href: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FBills%2F2026S1%2FPublic%2FHB1001.pdf#page=2 - title: Arkansas Legislature HB1001 status page (enacted as Act 1, 2026 1st Extraordinary Session) href: https://arkleg.state.ar.us/Bills/Detail?id=hb1001&ddBienniumSession=2025%2F2026S1 + - title: A.C.A. § 26-51-201(d)(1), as amended by Act 532 of 2023, § 2 (bracket amounts indexed annually, rounded to the nearest $100) + href: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2023R%2FPublic%2FACT532.pdf#page=4 + - title: A.C.A. § 26-51-201(a)(5), added by Act 1 of 2026 (1st Extraordinary Session), § 1 (the (a)(1)-(4) tables are indexed under (d)) + href: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2026S1%2FPublic%2FACT1.pdf#page=3 brackets: - threshold: @@ -59,7 +63,7 @@ brackets: uprating: parameter: gov.irs.uprating rounding: - type: downwards + type: nearest interval: 100 amount: 2014-01-01: 0.009 @@ -81,7 +85,7 @@ brackets: uprating: parameter: gov.irs.uprating rounding: - type: downwards + type: nearest interval: 100 amount: 2014-01-01: 0.025 @@ -105,7 +109,7 @@ brackets: uprating: parameter: gov.irs.uprating rounding: - type: downwards + type: nearest interval: 100 amount: 2014-01-01: 0.035 @@ -129,7 +133,7 @@ brackets: uprating: parameter: gov.irs.uprating rounding: - type: downwards + type: nearest interval: 100 amount: 2014-01-01: 0.045 @@ -154,7 +158,7 @@ brackets: uprating: parameter: gov.irs.uprating rounding: - type: downwards + type: nearest interval: 100 amount: 2014-01-01: 0.06 @@ -174,12 +178,6 @@ brackets: 2020-01-01: 38_500 2021-01-01: 39_700 2022-01-01: .inf - metadata: - uprating: - parameter: gov.irs.uprating - rounding: - type: downwards - interval: 100 amount: 2014-01-01: 0.07 2016-01-01: 0.06 @@ -194,12 +192,6 @@ brackets: 2019-01-01: 80_801 2020-01-01: 82_001 2021-01-01: .inf - metadata: - uprating: - parameter: gov.irs.uprating - rounding: - type: downwards - interval: 100 amount: 2014-01-01: .inf 2016-01-01: 0.069 diff --git a/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/reduction.yaml b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/reduction.yaml index e4f3df2d01c..d01d40670c5 100644 --- a/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/reduction.yaml +++ b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/reduction.yaml @@ -1,4 +1,4 @@ -description: Arkansas reduces the personal income tax by this amount, based on the taxable income. +description: Arkansas reduces the personal income tax by this amount, based on the taxable income, in years with a published Indexed Tax Brackets card. metadata: type: single_amount threshold_unit: currency-USD @@ -44,10 +44,9 @@ metadata: href: https://www.dfa.arkansas.gov/wp-content/uploads/2025_TaxBrackets.pdf#page=1 - title: Arkansas 2025 Individual Income Tax Forms and Instructions (2025 Regular Income Tax Table) href: https://www.dfa.arkansas.gov/wp-content/uploads/2025_AR1000F_and_AR1000NR_Instructions.pdf#page=30 - - title: Arkansas Act 1 of 2026 (HB1001, 1st Extraordinary Session), Section 1 amending A.C.A. § 26-51-201(a)(4)(C) - href: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FBills%2F2026S1%2FPublic%2FHB1001.pdf#page=2 - - title: Arkansas Legislature HB1001 status page (enacted as Act 1, 2026 1st Extraordinary Session) - href: https://arkleg.state.ar.us/Bills/Detail?id=hb1001&ddBienniumSession=2025%2F2026S1 +# From 2026 (use_published_reduction is false) the model derives the reduction +# from the statutory tables in rate.yaml and high_income/, indexed under +# A.C.A. § 26-51-201(d), so this schedule ends with the 2025 card. brackets: - threshold: 2014-01-01: 0 @@ -82,7 +81,6 @@ brackets: 2023-01-01: 5_300 2024-01-01: 5_500 2025-01-01: 5_600 - 2026-01-01: 5_600 amount: 2014-01-01: 68.78 2015-01-01: 64.49 @@ -96,7 +94,6 @@ brackets: 2023-01-01: 105.98 2024-01-01: 109.98 2025-01-01: 111.98 - 2026-01-01: 112 - threshold: 2014-01-01: 8_400 2015-01-01: 8_400 @@ -110,7 +107,6 @@ brackets: 2023-01-01: 10_600 2024-01-01: 10_900 2025-01-01: 11_200 - 2026-01-01: 11_200 amount: 2014-01-01: 152.77 2015-01-01: 148.48 @@ -124,7 +120,6 @@ brackets: 2023-01-01: 211.97 2024-01-01: 218.97 2025-01-01: 223.97 - 2026-01-01: 224 - threshold: 2014-01-01: 12_600 2015-01-01: 12_700 @@ -138,7 +133,6 @@ brackets: 2023-01-01: 15_100 2024-01-01: 15_600 2025-01-01: 16_000 - 2026-01-01: 16_000 amount: 2014-01-01: 278.76 2015-01-01: 275.47 @@ -152,7 +146,6 @@ brackets: 2023-01-01: 272.37 2024-01-01: 281.37 2025-01-01: 287.97 - 2026-01-01: 288 - threshold: 2014-01-01: 21_000 2015-01-01: 21_100 @@ -166,7 +159,6 @@ brackets: 2023-01-01: 25_000 2024-01-01: 25_700 2025-01-01: 26_400 - 2026-01-01: 26_400 amount: 2014-01-01: 593.75 2015-01-01: 596.25 @@ -180,7 +172,6 @@ brackets: 2023-01-01: 597.35 2024-01-01: 409.86 2025-01-01: 419.96 - 2026-01-01: 367.20 - threshold: 2014-01-01: 35_100 2015-01-01: 35_300 @@ -194,7 +185,6 @@ brackets: 2023-01-01: 89_601 2024-01-01: 92_301 2025-01-01: 94_701 - 2026-01-01: 94_701 amount: 2014-01-01: 944.74 2015-01-01: 949.24 @@ -208,7 +198,6 @@ brackets: 2023-01-01: 583.7 2024-01-01: 397.40 2025-01-01: 399.30 - 2026-01-01: 369.90 - threshold: 2014-01-01: .inf 2015-01-01: .inf @@ -222,7 +211,6 @@ brackets: 2023-01-01: 89_701 2024-01-01: 92_401 2025-01-01: 94_801 - 2026-01-01: 94_801 amount: 2014-01-01: 944.74 2015-01-01: 949.24 @@ -236,7 +224,6 @@ brackets: 2023-01-01: 573.7 2024-01-01: 387.40 2025-01-01: 389.30 - 2026-01-01: 359.90 - threshold: 2014-01-01: .inf 2015-01-01: .inf @@ -250,7 +237,6 @@ brackets: 2023-01-01: 89_801 2024-01-01: 92_501 2025-01-01: 94_901 - 2026-01-01: 94_901 amount: 2014-01-01: 944.74 2015-01-01: 949.24 @@ -264,7 +250,6 @@ brackets: 2023-01-01: 563.7 2024-01-01: 377.40 2025-01-01: 379.30 - 2026-01-01: 349.90 - threshold: 2014-01-01: .inf 2015-01-01: .inf @@ -278,7 +263,6 @@ brackets: 2023-01-01: 89_901 2024-01-01: 92_601 2025-01-01: 95_001 - 2026-01-01: 95_001 amount: 2014-01-01: 944.74 2015-01-01: 949.24 @@ -292,7 +276,6 @@ brackets: 2023-01-01: 553.7 2024-01-01: 367.40 2025-01-01: 369.30 - 2026-01-01: 339.90 - threshold: 2014-01-01: .inf 2015-01-01: .inf @@ -306,7 +289,6 @@ brackets: 2023-01-01: 90_001 2024-01-01: 92_701 2025-01-01: 95_101 - 2026-01-01: 95_101 amount: 2014-01-01: 944.74 2015-01-01: 949.24 @@ -320,7 +302,6 @@ brackets: 2023-01-01: 543.7 2024-01-01: 357.40 2025-01-01: 359.30 - 2026-01-01: 329.90 - threshold: 2014-01-01: .inf 2015-01-01: .inf @@ -334,7 +315,6 @@ brackets: 2023-01-01: 90_201 2024-01-01: 92_801 2025-01-01: 95_201 - 2026-01-01: 95_201 amount: 2014-01-01: 944.74 2015-01-01: 949.24 @@ -348,7 +328,6 @@ brackets: 2023-01-01: 533.7 2024-01-01: 347.40 2025-01-01: 349.30 - 2026-01-01: 319.90 - threshold: 2014-01-01: .inf 2015-01-01: .inf @@ -362,7 +341,6 @@ brackets: 2023-01-01: 90_301 2024-01-01: 92_901 2025-01-01: 95_301 - 2026-01-01: 95_301 amount: 2014-01-01: 944.74 2015-01-01: 949.24 @@ -376,371 +354,318 @@ brackets: 2023-01-01: 523.7 2024-01-01: 337.40 2025-01-01: 339.30 - 2026-01-01: 309.90 - threshold: 2021-01-01: .inf 2022-01-01: 87_801 2023-01-01: 90_401 2024-01-01: 93_001 2025-01-01: 95_401 - 2026-01-01: 95_401 amount: 2021-01-01: 247.5 2022-01-01: 557.20 2023-01-01: 513.7 2024-01-01: 327.40 2025-01-01: 329.30 - 2026-01-01: 299.90 - threshold: 2021-01-01: .inf 2022-01-01: 87_901 2023-01-01: 90_501 2024-01-01: 93_101 2025-01-01: 95_501 - 2026-01-01: 95_501 amount: 2021-01-01: 247.5 2022-01-01: 547.20 2023-01-01: 503.7 2024-01-01: 317.40 2025-01-01: 319.30 - 2026-01-01: 289.90 - threshold: 2021-01-01: .inf 2022-01-01: 88_001 2023-01-01: 90_601 2024-01-01: 93_201 2025-01-01: 95_601 - 2026-01-01: 95_601 amount: 2021-01-01: 247.5 2022-01-01: 537.20 2023-01-01: 493.7 2024-01-01: 307.40 2025-01-01: 309.30 - 2026-01-01: 279.90 - threshold: 2021-01-01: .inf 2022-01-01: 88_101 2023-01-01: 90_701 2024-01-01: 93_301 2025-01-01: 95_701 - 2026-01-01: 95_701 amount: 2021-01-01: 247.5 2022-01-01: 527.20 2023-01-01: 483.7 2024-01-01: 297.40 2025-01-01: 299.30 - 2026-01-01: 269.90 - threshold: 2021-01-01: .inf 2022-01-01: 88_201 2023-01-01: 90_801 2024-01-01: 93_401 2025-01-01: 95_801 - 2026-01-01: 95_801 amount: 2021-01-01: 247.5 2022-01-01: 517.20 2023-01-01: 473.7 2024-01-01: 287.40 2025-01-01: 289.30 - 2026-01-01: 259.90 - threshold: 2021-01-01: .inf 2022-01-01: 88_301 2023-01-01: 90_901 2024-01-01: 93_501 2025-01-01: 95_901 - 2026-01-01: 95_901 amount: 2021-01-01: 247.5 2022-01-01: 507.20 2023-01-01: 463.7 2024-01-01: 277.40 2025-01-01: 279.30 - 2026-01-01: 249.90 - threshold: 2021-01-01: .inf 2022-01-01: 88_401 2023-01-01: 91_101 2024-01-01: 93_601 2025-01-01: 96_001 - 2026-01-01: 96_001 amount: 2021-01-01: 247.5 2022-01-01: 497.20 2023-01-01: 453.7 2024-01-01: 267.40 2025-01-01: 269.30 - 2026-01-01: 239.90 - threshold: 2021-01-01: .inf 2022-01-01: 88_501 2023-01-01: 91_201 2024-01-01: 93_701 2025-01-01: 96_101 - 2026-01-01: 96_101 amount: 2021-01-01: 247.5 2022-01-01: 487.20 2023-01-01: 443.7 2024-01-01: 257.40 2025-01-01: 259.30 - 2026-01-01: 229.90 - threshold: 2021-01-01: .inf 2022-01-01: 88_601 2023-01-01: 91_301 2024-01-01: 93_801 2025-01-01: 96_201 - 2026-01-01: 96_201 amount: 2021-01-01: 247.5 2022-01-01: 477.20 2023-01-01: 433.7 2024-01-01: 247.40 2025-01-01: 249.30 - 2026-01-01: 219.90 - threshold: 2021-01-01: .inf 2022-01-01: 88_701 2023-01-01: 91_401 2024-01-01: 93_901 2025-01-01: 96_301 - 2026-01-01: 96_301 amount: 2021-01-01: 247.5 2022-01-01: 467.20 2023-01-01: 423.7 2024-01-01: 237.40 2025-01-01: 239.30 - 2026-01-01: 209.90 - threshold: 2021-01-01: .inf 2022-01-01: 88_801 2023-01-01: 91_501 2024-01-01: 94_001 2025-01-01: 96_401 - 2026-01-01: 96_401 amount: 2021-01-01: 247.5 2022-01-01: 457.20 2023-01-01: 413.7 2024-01-01: 227.40 2025-01-01: 229.30 - 2026-01-01: 199.90 - threshold: 2021-01-01: .inf 2022-01-01: 88_901 2023-01-01: 91_601 2024-01-01: 94_101 2025-01-01: 96_501 - 2026-01-01: 96_501 amount: 2021-01-01: 247.5 2022-01-01: 447.20 2023-01-01: 403.7 2024-01-01: 217.40 2025-01-01: 219.30 - 2026-01-01: 189.90 - threshold: 2021-01-01: .inf 2022-01-01: 89_001 2023-01-01: 91_701 2024-01-01: 94_201 2025-01-01: 96_601 - 2026-01-01: 96_601 amount: 2021-01-01: 247.5 2022-01-01: 437.20 2023-01-01: 393.7 2024-01-01: 207.40 2025-01-01: 209.30 - 2026-01-01: 179.90 - threshold: 2021-01-01: .inf 2022-01-01: 89_101 2023-01-01: 91_801 2024-01-01: 94_301 2025-01-01: 96_701 - 2026-01-01: 96_701 amount: 2021-01-01: 247.5 2022-01-01: 427.20 2023-01-01: 383.7 2024-01-01: 197.40 2025-01-01: 199.30 - 2026-01-01: 169.90 - threshold: 2021-01-01: .inf 2022-01-01: 89_201 2023-01-01: 91_901 2024-01-01: 94_501 2025-01-01: 96_801 - 2026-01-01: 96_801 amount: 2021-01-01: 247.5 2022-01-01: 417.20 2023-01-01: 373.7 2024-01-01: 187.40 2025-01-01: 189.30 - 2026-01-01: 159.90 - threshold: 2021-01-01: .inf 2022-01-01: 89_301 2023-01-01: 92_001 2024-01-01: 94_601 2025-01-01: 96_901 - 2026-01-01: 96_901 amount: 2021-01-01: 247.5 2022-01-01: 407.20 2023-01-01: 363.7 2024-01-01: 177.40 2025-01-01: 179.30 - 2026-01-01: 149.90 - threshold: 2021-01-01: .inf 2022-01-01: 89_401 2023-01-01: 92_101 2024-01-01: 94_701 2025-01-01: 97_001 - 2026-01-01: 97_001 amount: 2021-01-01: 247.5 2022-01-01: 397.20 2023-01-01: 353.7 2024-01-01: 167.40 2025-01-01: 169.30 - 2026-01-01: 139.90 - threshold: 2021-01-01: .inf 2022-01-01: 89_501 2023-01-01: 92_201 2024-01-01: 94_801 2025-01-01: 97_101 - 2026-01-01: 97_101 amount: 2021-01-01: 247.5 2022-01-01: 387.20 2023-01-01: 343.7 2024-01-01: 157.40 2025-01-01: 159.30 - 2026-01-01: 129.90 - threshold: 2021-01-01: .inf 2022-01-01: 89_601 2023-01-01: 92_301 2024-01-01: 94_901 2025-01-01: 97_201 - 2026-01-01: 97_201 amount: 2021-01-01: 247.5 2022-01-01: 377.20 2023-01-01: 333.7 2024-01-01: 147.40 2025-01-01: 149.30 - 2026-01-01: 119.90 - threshold: 2021-01-01: .inf 2022-01-01: 89_701 2023-01-01: 92_401 2024-01-01: 95_001 2025-01-01: 97_301 - 2026-01-01: 97_301 amount: 2021-01-01: 247.5 2022-01-01: 367.20 2023-01-01: 323.7 2024-01-01: 137.40 2025-01-01: 139.30 - 2026-01-01: 109.90 - threshold: 2021-01-01: .inf 2022-01-01: 89_801 2023-01-01: 92_501 2024-01-01: 95_101 2025-01-01: 97_401 - 2026-01-01: 97_401 amount: 2021-01-01: 247.5 2022-01-01: 357.20 2023-01-01: 313.7 2024-01-01: 127.40 2025-01-01: 129.30 - 2026-01-01: 99.90 - threshold: 2021-01-01: .inf 2022-01-01: 89_901 2023-01-01: 92_601 2024-01-01: 95_201 2025-01-01: 97_501 - 2026-01-01: 97_501 amount: 2021-01-01: 247.5 2022-01-01: 347.20 2023-01-01: 303.7 2024-01-01: 117.40 2025-01-01: 119.30 - 2026-01-01: 89.90 - threshold: 2021-01-01: .inf 2022-01-01: 90_001 2023-01-01: 92_701 2024-01-01: 95_301 2025-01-01: 97_601 - 2026-01-01: 97_601 amount: 2021-01-01: 247.5 2022-01-01: 337.20 2023-01-01: 293.7 2024-01-01: 107.40 2025-01-01: 109.30 - 2026-01-01: 79.90 - threshold: 2021-01-01: .inf 2022-01-01: 90_101 2023-01-01: 92_801 2024-01-01: 95_401 2025-01-01: 97_701 - 2026-01-01: .inf amount: 2021-01-01: 247.5 2022-01-01: 327.20 2023-01-01: 283.7 2024-01-01: 97.40 2025-01-01: 99.30 - 2026-01-01: 79.90 - threshold: 2021-01-01: .inf 2022-01-01: 90_201 2023-01-01: 92_901 2024-01-01: 95_501 2025-01-01: 97_801 - 2026-01-01: .inf amount: 2021-01-01: 247.5 2022-01-01: 317.20 2023-01-01: 273.7 2024-01-01: 87.40 2025-01-01: 89.30 - 2026-01-01: 79.90 - threshold: 2021-01-01: .inf 2022-01-01: 90_301 2023-01-01: 93_001 2024-01-01: .inf 2025-01-01: .inf - 2026-01-01: .inf amount: 2021-01-01: 247.5 2022-01-01: 307.20 2023-01-01: 263.7 2024-01-01: 87.40 2025-01-01: 89.30 - 2026-01-01: 79.90 - threshold: 2021-01-01: .inf 2022-01-01: 90_401 @@ -753,7 +678,6 @@ brackets: 2023-01-01: 253.7 2024-01-01: 87.40 2025-01-01: 89.30 - 2026-01-01: 79.90 - threshold: 2021-01-01: .inf 2022-01-01: 90_501 @@ -766,7 +690,6 @@ brackets: 2023-01-01: 243.7 2024-01-01: 87.40 2025-01-01: 89.30 - 2026-01-01: 79.90 - threshold: 2021-01-01: .inf 2022-01-01: 90_601 @@ -779,7 +702,6 @@ brackets: 2023-01-01: 233.7 2024-01-01: 87.40 2025-01-01: 89.30 - 2026-01-01: 79.90 - threshold: 2021-01-01: .inf 2022-01-01: 90_701 @@ -792,7 +714,6 @@ brackets: 2023-01-01: 223.7 2024-01-01: 87.40 2025-01-01: 89.30 - 2026-01-01: 79.90 - threshold: 2021-01-01: .inf 2022-01-01: 90_801 @@ -805,7 +726,6 @@ brackets: 2023-01-01: 213.7 2024-01-01: 87.40 2025-01-01: 89.30 - 2026-01-01: 79.90 - threshold: 2021-01-01: .inf 2022-01-01: 90_901 @@ -818,7 +738,6 @@ brackets: 2023-01-01: 203.7 2024-01-01: 87.40 2025-01-01: 89.30 - 2026-01-01: 79.90 - threshold: 2021-01-01: .inf 2022-01-01: 91_101 @@ -831,7 +750,6 @@ brackets: 2023-01-01: 193.7 2024-01-01: 87.40 2025-01-01: 89.30 - 2026-01-01: 79.90 - threshold: 2021-01-01: .inf 2022-01-01: 91_201 @@ -844,7 +762,6 @@ brackets: 2023-01-01: 183.7 2024-01-01: 87.40 2025-01-01: 89.30 - 2026-01-01: 79.90 - threshold: 2021-01-01: .inf 2022-01-01: 91_301 @@ -857,7 +774,6 @@ brackets: 2023-01-01: 173.7 2024-01-01: 87.40 2025-01-01: 89.30 - 2026-01-01: 79.90 - threshold: 2021-01-01: .inf 2022-01-01: 91_401 @@ -870,7 +786,6 @@ brackets: 2023-01-01: 163.7 2024-01-01: 87.40 2025-01-01: 89.30 - 2026-01-01: 79.90 - threshold: 2021-01-01: .inf 2022-01-01: 91_501 @@ -883,7 +798,6 @@ brackets: 2023-01-01: 153.7 2024-01-01: 87.40 2025-01-01: 89.30 - 2026-01-01: 79.90 - threshold: 2021-01-01: .inf 2022-01-01: 91_601 @@ -896,7 +810,6 @@ brackets: 2023-01-01: 153.7 2024-01-01: 87.40 2025-01-01: 89.30 - 2026-01-01: 79.90 - threshold: 2021-01-01: .inf 2022-01-01: 91_701 @@ -909,7 +822,6 @@ brackets: 2023-01-01: 153.70 2024-01-01: 87.40 2025-01-01: 89.30 - 2026-01-01: 79.90 - threshold: 2021-01-01: .inf 2022-01-01: 91_801 @@ -922,7 +834,6 @@ brackets: 2023-01-01: 153.70 2024-01-01: 87.40 2025-01-01: 89.30 - 2026-01-01: 79.90 - threshold: 2022-01-01: .inf 2023-01-01: .inf @@ -933,4 +844,3 @@ brackets: 2023-01-01: 153.70 2024-01-01: 87.40 2025-01-01: 89.30 - 2026-01-01: 79.90 diff --git a/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/use_published_reduction.yaml b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/use_published_reduction.yaml new file mode 100644 index 00000000000..cf6afbdae0a --- /dev/null +++ b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/use_published_reduction.yaml @@ -0,0 +1,15 @@ +description: Arkansas computes the regular income tax as the published rate times net taxable income minus the published reduction if this is true, and from the statutory tax tables as indexed otherwise. +values: + 0000-01-01: true + # No Indexed Tax Brackets card has been published for 2026 or later, so the + # model derives those years from A.C.A. § 26-51-201(a)(4), indexed under (d). + 2026-01-01: false +metadata: + unit: bool + period: year + label: Arkansas published income tax reduction schedule applies + reference: + - title: 2025 Indexed Tax Brackets + href: https://www.dfa.arkansas.gov/wp-content/uploads/2025_TaxBrackets.pdf#page=1 + - title: Arkansas Act 1 of 2026 (1st Extraordinary Session), § 1, amending A.C.A. § 26-51-201(a)(4) and adding (a)(5) + href: https://www.arkleg.state.ar.us/Home/FTPDocument?path=%2FACTS%2F2026S1%2FPublic%2FACT1.pdf#page=1 diff --git a/policyengine_us/tests/core/test_state_uprating_rounding.py b/policyengine_us/tests/core/test_state_uprating_rounding.py index ac01c14b510..083c61f6dad 100644 --- a/policyengine_us/tests/core/test_state_uprating_rounding.py +++ b/policyengine_us/tests/core/test_state_uprating_rounding.py @@ -11,17 +11,40 @@ def _thresholds(scale, period, bracket_indexes=(1, 2, 3)): return tuple(scale.brackets[index].threshold(period) for index in bracket_indexes) -def test_ar_income_tax_thresholds_round_down_after_last_published_year(): - scale = SYSTEM.parameters.gov.states.ar.tax.income.rates.main.rate - - assert _thresholds(scale, "2027-01-01", (1, 2, 3, 4)) == ( - 5_700, - 11_500, - 16_400, - 27_100, - ) - assert isinf(scale.brackets[5].threshold("2027-01-01")) - assert isinf(scale.brackets[6].threshold("2027-01-01")) +def test_ar_income_tax_bounds_round_to_nearest_100_after_last_published_year(): + # A.C.A. 26-51-201(d)(1): bracket amounts are indexed "rounding to the + # nearest one hundred dollars ($100)". Expected values follow the loaded + # index, so a CPI refresh moves them without breaking the test. + main = SYSTEM.parameters.gov.states.ar.tax.income.rates.main + uprating = SYSTEM.parameters.gov.irs.uprating + high_income = main.high_income + bounds_2026 = { + "rate": (main.rate, (1, 2, 3, 4), (5_600, 11_200, 16_000, 26_400)), + "high-income threshold": (None, None, (94_700,)), + "(B) 2% row top": (high_income.rate, (1,), (4_700,)), + "(C) rows": ( + high_income.bracket_adjustment, + tuple(range(30)), + tuple(range(94_700, 97_601, 100)), + ), + } + differs_from_rounding_down = False + for year in range(2027, 2036): + period = f"{year}-01-01" + factor = uprating(period) / uprating("2026-01-01") + for name, (scale, indexes, bases) in bounds_2026.items(): + if scale is None: + loaded = (high_income.threshold(period),) + else: + loaded = _thresholds(scale, period, indexes) + expected = tuple(round(base * factor / 100) * 100 for base in bases) + assert loaded == expected, (year, name) + rounded_down = tuple(base * factor // 100 * 100 for base in bases) + differs_from_rounding_down |= loaded != rounded_down + assert isinf(main.rate.brackets[5].threshold(period)) + assert isinf(main.rate.brackets[6].threshold(period)) + # With 2027's factor, 5,600 x 1.0298 = 5,766.9 rounds up to 5,800. + assert differs_from_rounding_down def test_nm_low_income_rebate_amounts_round_to_whole_dollars(): diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.yaml index 4e28a6e37c1..955f915ea9f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.yaml @@ -494,3 +494,97 @@ # DFA 2024 Indexed Tax Brackets row "$95,501 and over 3.90% $87.40": # 0.039 x 95,501 - 87.40 = 3,637.14 ar_income_tax_before_non_refundable_credits_indiv: 3_637.14 + +# 2027 cases use incomes whose tax survives a one-step change in the projected +# uprating factor. The (B)-(C) rows near the indexed threshold (about $97,500) +# sit within a few hundredths of a percent of a rounding flip, so +# test_ar_income_tax_schedule.py checks them against the loaded index instead. +- name: AR 2027 indexed Act 1 of 2026 tables - top of the 0% row + period: 2027 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 5_799 + state_code: AR + output: + # the indexed 2% row starts at $5,800 (5,600 x 1.0298 = 5,767, nearest $100) + ar_income_tax_before_non_refundable_credits_indiv: 0 + +- name: AR 2027 indexed Act 1 of 2026 tables - 2% row + period: 2027 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 5_900 + state_code: AR + output: + # (5,900 - 5,800) x 2% = 2.00 + ar_income_tax_before_non_refundable_credits_indiv: 2 + +- name: AR 2027 indexed Act 1 of 2026 tables - income the frozen 2026 reduction undertaxed by $108 + period: 2027 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 11_300 + state_code: AR + output: + # (11,300 - 5,800) x 2% = 110.00; the frozen 2026 reduction schedule gave 2% x 11,300 - 224 = 2.00 + ar_income_tax_before_non_refundable_credits_indiv: 110 + +- name: AR 2027 indexed Act 1 of 2026 tables - start of the 3% row + period: 2027 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 11_500 + state_code: AR + output: + # (11,500 - 5,800) x 2% = 114.00 + ar_income_tax_before_non_refundable_credits_indiv: 114 + +- name: AR 2027 indexed Act 1 of 2026 tables - start of the 3.4% row + period: 2027 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 16_500 + state_code: AR + output: + # 114 + (16,500 - 11,500) x 3% = 264.00 + ar_income_tax_before_non_refundable_credits_indiv: 264 + +- name: AR 2027 indexed Act 1 of 2026 tables - income the frozen 2026 reduction undertaxed by $70.20 + period: 2027 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 26_500 + state_code: AR + output: + # 264 + (26,500 - 16,500) x 3.4% = 604.00; the frozen 2026 reduction schedule gave 3.4% x 26,500 - 367.20 = 533.80 + ar_income_tax_before_non_refundable_credits_indiv: 604 + +- name: AR 2027 indexed Act 1 of 2026 tables - start of the 3.7% row + period: 2027 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 27_200 + state_code: AR + output: + # 264 + (27,200 - 16,500) x 3.4% = 627.80 + ar_income_tax_before_non_refundable_credits_indiv: 627.80 + +- name: AR 2027 indexed Act 1 of 2026 tables - top of table (A) at the indexed high-income threshold + period: 2027 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 97_500 + state_code: AR + output: + # 627.80 + (97,500 - 27,200) x 3.7% = 3,228.90; the threshold is 94,700 x 1.0298 = 97,522, nearest $100 + ar_income_tax_before_non_refundable_credits_indiv: 3_228.90 + +- name: AR 2027 indexed Act 1 of 2026 tables - high income + period: 2027 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 150_000 + state_code: AR + output: + # 96 + (150,000 - 4,800) x 3.7% = 5,468.40 + ar_income_tax_before_non_refundable_credits_indiv: 5_468.40 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_joint.yaml index 96d1a5f83b0..3429a1c3b37 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_joint.yaml @@ -199,3 +199,15 @@ output: # 2024 published last row $35,001-$35,100 -> $765 ar_income_tax_before_non_refundable_credits_joint: [765, 0, 0, 0] + +- name: AR 2027 indexed Act 1 of 2026 tables - joint return above the low-income tables + period: 2027 + absolute_error_margin: 0.01 + input: + ar_taxable_income_joint: 50_000 + ar_low_income_tax_joint: 5_000 + state_code: AR + output: + # Indexed 2027 rows start at 5,800 / 11,500 / 16,500 / 27,200 (nearest $100): + # 5,700 x 2% + 5,000 x 3% + 10,700 x 3.4% + (50,000 - 27,200) x 3.7% = 1,471.40 + ar_income_tax_before_non_refundable_credits_joint: 1_471.40 diff --git a/policyengine_us/tests/test_ar_income_tax_schedule.py b/policyengine_us/tests/test_ar_income_tax_schedule.py index 3e77abb8779..4e865b8574d 100644 --- a/policyengine_us/tests/test_ar_income_tax_schedule.py +++ b/policyengine_us/tests/test_ar_income_tax_schedule.py @@ -1,12 +1,23 @@ """Arkansas regular income tax schedule: differential and invariant tests. -`ar_income_tax_before_non_refundable_credits_indiv` computes -`rate.calc(ti) * ti - reduction.calc(ti)` from two single_amount scales whose -rows are shared across years, so a mis-keyed row in one year's column (a -skipped DFA row, an ignored asterisked two-row range, a duplicated -threshold) silently shifts every later bracket. These tests compare every -integer taxable income against an independent transcription of each year's -source: +`ar_main_income_tax` (shared by the indiv and joint tax variables and +`ar_uses_low_income_tax_tables`) has two paths: + +- Years with a published DFA Indexed Tax Brackets card (2021-2025) compute + `rate.calc(ti) * ti - reduction.calc(ti)` from two single_amount scales + whose rows are shared across years, so a mis-keyed row in one year's column + (a skipped DFA row, an ignored asterisked two-row range, a duplicated + threshold) silently shifts every later bracket. +- Later years apply the statutory tables of A.C.A. 26-51-201(a)(4) (Act 1 of + 2026): (A) marginal rates up to the high-income threshold, and above it the + (B) marginal rates less the (C) bracket adjustment. 26-51-201(a)(5) and + (d)(1) index every bracket bound each year, "rounding to the nearest one + hundred dollars ($100)"; the (C) dollar amounts are not bracket bounds and + stay fixed. The reduction is derived from the same indexed tables, so it + moves with the thresholds (a stored reduction would not). + +Differential tests compare every integer taxable income against an +independent transcription of each year's source: - 2021-2025: DFA Indexed Tax Brackets cards, e.g. https://www.dfa.arkansas.gov/wp-content/uploads/2025_TaxBrackets.pdf#page=1 @@ -14,22 +25,47 @@ minus adjustment falls $10 per row. Asterisked rows span $200, so the next row's start is skipped. - 2026: Act 1 of 2026 (1st Extraordinary Session), A.C.A. 26-51-201(a)(4). +- 2027-2035: Act 1 of 2026's tables with every bracket bound indexed from + 2026 by PolicyEngine's uprating index and rounded to the nearest $100. -Invariants (2021-2026): reduction.yaml rows carry strictly increasing -finite thresholds, with every .inf row after them. At every integer taxable -income, tax is non-negative and never falls as income rises, except the one -drop Act 1 of 2026 writes into the statute: -(a)(4)(A) taxes $94,700 at $3,136.70, while (a)(4)(B) less the $290 -(a)(4)(C) adjustment taxes $94,701 at $3,134.04. +Invariants: +- reduction.yaml rows carry strictly increasing finite thresholds, with every + .inf row after them (published years). +- At every integer taxable income, tax is non-negative and never falls as + income rises (2021-2040, 2050, 2075, 2100), except where the statute's own + (A) and (B)-(C) tables meet. In 2026 that seam drops: (a)(4)(A) taxes + $94,700 at $3,136.70, while (a)(4)(B) less the $290 (a)(4)(C) adjustment + taxes $94,701 at $3,134.04. Once indexing raises the (A) bounds, the seam + rises instead. +- The (C) ladder starts at the high-income threshold, its bounds strictly + increase, and its amounts stay Act 1's $290-$10 steps; the (B) top rate + equals the (A) top rate (2026-2040, 2050, 2075, 2100). +- Property tests over random tables: the derived (A) tax equals + rate x income - the sum of (rate step x row start), is continuous at every + row start, and tax never falls within either regime. +- A reform to the rate table in a projected year keeps tax from falling + anywhere except where (A) meets (B)-(C), because the reduction is derived + rather than stored. """ +from types import SimpleNamespace + import numpy as np import pytest +from policyengine_core.taxscales import MarginalRateTaxScale, SingleAmountTaxScale from policyengine_us.system import system +from policyengine_us.variables.gov.states.ar.tax.income.ar_income_tax_helpers import ( + ar_main_income_tax, +) P = system.parameters.gov.states.ar.tax.income.rates.main -YEARS = range(2021, 2027) +UPRATING = system.parameters.gov.irs.uprating +PUBLISHED_YEARS = range(2021, 2026) +SOURCE_YEARS = range(2021, 2036) +LONG_RUN_YEARS = [2050, 2075, 2100] +INVARIANT_YEARS = list(range(2021, 2041)) + LONG_RUN_YEARS +DERIVED_YEARS = list(range(2026, 2041)) + LONG_RUN_YEARS TAXABLE_INCOME = np.concatenate( [np.arange(0, 150_001, dtype=float), [250_000.0, 1_000_000.0]] ) @@ -132,16 +168,56 @@ def act_1_of_2026_tax(ti): return np.where(ti <= 94_700, low, high) -def model_tax(year, ti): - instant = f"{year}-01-01" - return P.rate(instant).calc(ti) * ti - P.reduction(instant).calc(ti) +# Act 1 of 2026, A.C.A. 26-51-201(a)(4): (A) row starts and rates; the +# (A)/(B) threshold; the top of (B)'s 2% row; and each (C) row as the bound it +# starts above ("From" - 1) with its bracket adjustment amount. +ACT_1_TABLE_A = [(5_600, 0.02), (11_200, 0.03), (16_000, 0.034), (26_400, 0.037)] +ACT_1_THRESHOLD = 94_700 +ACT_1_TABLE_B_2_PERCENT_TOP = 4_700 +ACT_1_LADDER = [(94_700 + 100 * k, 290 - 10 * k) for k in range(29)] + [(97_600, 0)] + + +def nearest_100(amount): + # 26-51-201(d)(1): "rounding to the nearest one hundred dollars ($100)". + return np.floor(amount / 100 + 0.5) * 100 + + +def indexed_act_1_of_2026_tax(year, ti): + """Act 1 of 2026's tables, every bracket bound indexed from 2026.""" + factor = UPRATING(f"{year}-01-01") / UPRATING("2026-01-01") + starts = [nearest_100(start * factor) for start, _ in ACT_1_TABLE_A] + ends = starts[1:] + [np.inf] + rates = [rate for _, rate in ACT_1_TABLE_A] + table_a = sum( + rate * np.clip(ti - start, 0, end - start) + for start, end, rate in zip(starts, ends, rates) + ) + top_2_percent = nearest_100(ACT_1_TABLE_B_2_PERCENT_TOP * factor) + table_b = 0.02 * np.minimum(ti, top_2_percent) + 0.037 * np.maximum( + ti - top_2_percent, 0 + ) + bounds = np.array([nearest_100(bound * factor) for bound, _ in ACT_1_LADDER]) + amounts = np.array([amount for _, amount in ACT_1_LADDER]) + # Row k covers income above bounds[k] up to and including bounds[k + 1]. + row = np.searchsorted(bounds, ti, side="left") - 1 + adjustment = np.where(row >= 0, amounts[np.maximum(row, 0)], 0) + threshold = nearest_100(ACT_1_THRESHOLD * factor) + return np.where(ti <= threshold, table_a, table_b - adjustment) + + +def model_tax(year, ti, parameters=P): + return ar_main_income_tax(ti, parameters(f"{year}-01-01")) def source_tax(year, ti): - return act_1_of_2026_tax(ti) if year == 2026 else card_tax(year, ti) + if year in PUBLISHED_YEARS: + return card_tax(year, ti) + if year == 2026: + return act_1_of_2026_tax(ti) + return indexed_act_1_of_2026_tax(year, ti) -@pytest.mark.parametrize("year", YEARS) +@pytest.mark.parametrize("year", SOURCE_YEARS) def test_ar_schedule_matches_source_at_every_integer_income(year): diff = model_tax(year, TAXABLE_INCOME) - source_tax(year, TAXABLE_INCOME) bad = np.flatnonzero(np.abs(diff) > 0.005) @@ -151,24 +227,59 @@ def test_ar_schedule_matches_source_at_every_integer_income(year): ) +def test_ar_indexed_reference_is_act_1_of_2026_in_2026(): + diff = indexed_act_1_of_2026_tax(2026, TAXABLE_INCOME) - act_1_of_2026_tax( + TAXABLE_INCOME + ) + assert np.abs(diff).max() < 1e-6 + + +def test_ar_published_reduction_applies_through_2025_only(): + assert [ + bool(P.use_published_reduction(f"{y}-01-01")) for y in range(2021, 2028) + ] == [True] * 5 + [False] * 2 + + +@pytest.mark.parametrize("year", [2000, 2013]) +def test_ar_schedule_before_modeled_years_still_computes(year): + # rate.yaml starts in 2014; earlier years must not hit the statutory path, + # whose parameters start in 2026. + assert bool(P.use_published_reduction(f"{year}-01-01")) + assert (model_tax(year, TAXABLE_INCOME) == 0).all() + + # {income: change in tax from income to income + 1} where the statute itself # makes tax fall: A.C.A. 26-51-201(a)(4)(A) vs (a)(4)(B)-(C), Act 1 of 2026. STATUTORY_DROPS = {2026: {94_700: -2.66}} -@pytest.mark.parametrize("year", YEARS) +def invariant_grid(year): + # Every integer up to well past the (C) ladder, which indexing moves up. + top = 150_000 + if year not in PUBLISHED_YEARS: + top = max(top, int(1.6 * P.high_income.threshold(f"{year}-01-01"))) + extra = [x for x in (250_000.0, 1_000_000.0, 1e7) if x > top] + return np.concatenate([np.arange(0, top + 1, dtype=float), extra]) + + +@pytest.mark.parametrize("year", INVARIANT_YEARS) def test_ar_schedule_tax_is_nonnegative_and_nondecreasing(year): - tax = model_tax(year, TAXABLE_INCOME) + ti = invariant_grid(year) + tax = model_tax(year, ti) assert tax.min() >= -1e-9 change = np.diff(tax) falls = { - int(TAXABLE_INCOME[i]): round(float(change[i]), 2) - for i in np.flatnonzero(change < -1e-9) + int(ti[i]): round(float(change[i]), 2) for i in np.flatnonzero(change < -1e-9) } - assert falls == STATUTORY_DROPS.get(year, {}) + if year in STATUTORY_DROPS or year in PUBLISHED_YEARS: + assert falls == STATUTORY_DROPS.get(year, {}) + else: + # Only where the statute's (A) and (B)-(C) tables meet. + seam = P.high_income.threshold(f"{year}-01-01") + assert set(falls) <= {seam}, falls -@pytest.mark.parametrize("year", YEARS) +@pytest.mark.parametrize("year", PUBLISHED_YEARS) def test_ar_reduction_rows_have_strictly_increasing_thresholds(year): # Read each YAML row's own threshold: the loaded scale can't show a # mis-keyed row, because add_bracket sorts thresholds and sums the amounts @@ -178,3 +289,101 @@ def test_ar_reduction_rows_have_strictly_increasing_thresholds(year): finite = [t for t in thresholds if t < np.inf] assert thresholds[: len(finite)] == finite, thresholds assert all(a < b for a, b in zip(finite, finite[1:])), finite + + +@pytest.mark.parametrize("year", DERIVED_YEARS) +def test_ar_bracket_adjustment_ladder_follows_the_indexed_threshold(year): + # Read each YAML row, as above: the loaded scale would hide a duplicate. + instant = f"{year}-01-01" + rows = P.high_income.bracket_adjustment.brackets + bounds = [row.threshold(instant) for row in rows] + amounts = [row.amount(instant) for row in rows] + assert bounds[0] == P.high_income.threshold(instant) + widths = np.diff(bounds) + assert (widths >= 100).all() and (widths % 100 == 0).all(), bounds + assert amounts == [amount for _, amount in ACT_1_LADDER] + + +@pytest.mark.parametrize("year", DERIVED_YEARS) +def test_ar_high_income_top_rate_matches_rate_table(year): + p = P(f"{year}-01-01") + finite_rates = [ + rate + for threshold, rate in zip(p.rate.thresholds, p.rate.amounts) + if threshold < np.inf + ] + assert p.high_income.rate.rates[-1] == finite_rates[-1] + + +def random_statutory_tables(rng): + """Random (A), (B) and (C) tables shaped like 26-51-201(a)(4).""" + starts = np.sort(rng.choice(np.arange(1, 500), rng.integers(1, 7), replace=False)) + table_a = SingleAmountTaxScale() + table_a.add_bracket(0, 0) + for start, rate in zip(100 * starts, np.sort(rng.uniform(0, 0.1, starts.size))): + table_a.add_bracket(int(start), float(rate)) + threshold = 100 * int(starts[-1] + rng.integers(1, 1_000)) + table_b = MarginalRateTaxScale() + low_rate, high_rate = np.sort(rng.uniform(0, 0.1, 2)) + table_b.add_bracket(0, float(low_rate)) + table_b.add_bracket(100 * int(rng.integers(1, threshold // 100)), float(high_rate)) + rows = int(rng.integers(1, 40)) + bounds = threshold + np.concatenate([[0], np.cumsum(rng.choice([100, 200], rows))]) + amounts = np.append(np.sort(rng.uniform(0, 500, rows))[::-1], 0) + ladder = SingleAmountTaxScale() + for bound, amount in zip(bounds, amounts): + ladder.add_bracket(int(bound), float(amount)) + return SimpleNamespace( + use_published_reduction=False, + rate=table_a, + high_income=SimpleNamespace( + threshold=threshold, rate=table_b, bracket_adjustment=ladder + ), + ) + + +@pytest.mark.parametrize("seed", range(200)) +def test_ar_derived_schedule_properties_on_random_tables(seed): + p = random_statutory_tables(np.random.default_rng(seed)) + threshold = p.high_income.threshold + ti = np.arange(0, threshold + 10_000, dtype=float) + tax = ar_main_income_tax(ti, p) + table_a = ti <= threshold + # The derived (A) tax is the card form: rate x income minus the reduction + # that the indexed row starts imply. + starts = np.array(p.rate.thresholds) + steps = np.diff(p.rate.amounts, prepend=0) + reduction = (steps * starts * (ti[:, None] >= starts)).sum(axis=1) + card_form = p.rate.calc(ti) * ti - reduction + assert np.abs(tax[table_a] - card_form[table_a]).max() < 1e-9 + # Continuous at every (A) row start: the step there is the prior rate. + for start, prior_rate in zip(starts[1:], p.rate.amounts[:-1]): + if 0 < start <= threshold: + step = tax[int(start)] - tax[int(start) - 1] + assert abs(step - prior_rate) < 1e-9 + # (A) is never negative. Above the threshold that depends on the (C) + # amounts being small next to the (B) tax, which the statute's are (checked + # per year above) but random ones need not be. Neither regime ever falls. + assert tax[table_a].min() >= -1e-9 + change = np.diff(tax) + within_regime = table_a[1:] == table_a[:-1] + assert (change[within_regime] >= -1e-9).all() + + +def test_ar_rate_table_reform_keeps_projected_tax_from_falling(): + # A stored 2027 reduction would ignore this reform and make tax fall at + # the moved bounds; the derived one follows it. + reformed = P.clone() + reformed.rate.brackets[1].threshold.update(period="year:2027-01-01:1", value=7_000) + reformed.rate.brackets[4].amount.update(period="year:2027-01-01:1", value=0.035) + reformed.high_income.rate.brackets[1].rate.update( + period="year:2027-01-01:1", value=0.035 + ) + ti = np.arange(0, 150_001, dtype=float) + tax = model_tax(2027, ti, reformed) + assert tax[7_000] == pytest.approx(0) + assert tax[7_100] == pytest.approx(2) + change = np.diff(tax) + falls = ti[:-1][change < -1e-9] + seam = reformed.high_income.threshold("2027-01-01") + assert set(falls) <= {seam}, falls diff --git a/policyengine_us/variables/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.py b/policyengine_us/variables/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.py index 11ab1ceda1d..07e8810ce18 100644 --- a/policyengine_us/variables/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.py +++ b/policyengine_us/variables/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_indiv.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.ar.tax.income.ar_income_tax_helpers import ( + ar_main_income_tax, +) class ar_income_tax_before_non_refundable_credits_indiv(Variable): @@ -16,7 +19,4 @@ class ar_income_tax_before_non_refundable_credits_indiv(Variable): def formula(person, period, parameters): p = parameters(period).gov.states.ar.tax.income.rates.main taxable_income = person("ar_taxable_income_indiv", period) - main_rate = p.rate.calc(taxable_income) - pre_reduction_tax = main_rate * taxable_income - reduction = p.reduction.calc(taxable_income) - return max_(pre_reduction_tax - reduction, 0) + return max_(ar_main_income_tax(taxable_income, p), 0) diff --git a/policyengine_us/variables/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_joint.py b/policyengine_us/variables/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_joint.py index 896a9bc9070..de48f8af0f8 100644 --- a/policyengine_us/variables/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_joint.py +++ b/policyengine_us/variables/gov/states/ar/tax/income/ar_income_tax_before_non_refundable_credits_joint.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.ar.tax.income.ar_income_tax_helpers import ( + ar_main_income_tax, +) class ar_income_tax_before_non_refundable_credits_joint(Variable): @@ -18,10 +21,6 @@ class ar_income_tax_before_non_refundable_credits_joint(Variable): def formula(person, period, parameters): p = parameters(period).gov.states.ar.tax.income.rates.main taxable_income = person("ar_taxable_income_joint", period) - - main_rate = p.rate.calc(taxable_income) - pre_reduction_tax = main_rate * taxable_income - reduction = p.reduction.calc(taxable_income) - total_main_rate = max_(pre_reduction_tax - reduction, 0) + total_main_rate = max_(ar_main_income_tax(taxable_income, p), 0) low_income_tax = person("ar_low_income_tax_joint", period) return min_(total_main_rate, low_income_tax) diff --git a/policyengine_us/variables/gov/states/ar/tax/income/ar_income_tax_helpers.py b/policyengine_us/variables/gov/states/ar/tax/income/ar_income_tax_helpers.py new file mode 100644 index 00000000000..d03274be3c8 --- /dev/null +++ b/policyengine_us/variables/gov/states/ar/tax/income/ar_income_tax_helpers.py @@ -0,0 +1,51 @@ +from policyengine_us.model_api import * + + +def ar_main_income_tax(taxable_income, p): + """Arkansas regular income tax on net taxable income, before the $0 floor. + + `p` is `parameters(period).gov.states.ar.tax.income.rates.main`. + + Years with a published DFA Indexed Tax Brackets card apply the card: + rate times taxable income, minus the card's reduction ("minus adjustment"). + + Later years apply the statutory tables of A.C.A. 26-51-201(a)(4), whose + bracket bounds 26-51-201(a)(5) and (d)(1) index each year, rounded to the + nearest $100: + - (A) net income at or below the high-income threshold: the rate table at + marginal rates, each from its row's lower bound; + - (B) net income above it: the high-income marginal rate table, applied to + all of that income, + - (C) less the fixed bracket adjustment for the row the income falls in. + Taxing at marginal rates keeps tax continuous at every (A) and (B) row bound + wherever indexing moves them; a stored reduction would not move with them. + As in the statute, tax still steps where (A) meets (B)-(C) (down $2.66 at + 2026's $94,700) and rises by the $10 adjustment step at each (C) row bound. + """ + if p.use_published_reduction: + rate = p.rate.calc(taxable_income) + return rate * taxable_income - p.reduction.calc(taxable_income) + high_income = p.high_income + table_a_tax = marginal_tax(p.rate, taxable_income) + table_b_tax = high_income.rate.calc(taxable_income) + # Thresholds are the "Less Than or Equal To" bound of the prior row. + bracket_adjustment = high_income.bracket_adjustment.calc(taxable_income, right=True) + return where( + taxable_income <= high_income.threshold, + table_a_tax, + table_b_tax - bracket_adjustment, + ) + + +def marginal_tax(rate_scale, income): + """Tax at the rates of a single_amount scale keyed by each row's lower bound, + applied to the income within each row.""" + thresholds = rate_scale.thresholds + rates = rate_scale.amounts + tax = 0 + for i, (lower, rate) in enumerate(zip(thresholds, rates)): + if lower == np.inf: + break + upper = thresholds[i + 1] if i + 1 < len(thresholds) else np.inf + tax += rate * np.clip(income - lower, 0, upper - lower) + return tax diff --git a/policyengine_us/variables/gov/states/ar/tax/income/ar_uses_low_income_tax_tables.py b/policyengine_us/variables/gov/states/ar/tax/income/ar_uses_low_income_tax_tables.py index e1eaf9712d9..8e7563c3cb2 100644 --- a/policyengine_us/variables/gov/states/ar/tax/income/ar_uses_low_income_tax_tables.py +++ b/policyengine_us/variables/gov/states/ar/tax/income/ar_uses_low_income_tax_tables.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.states.ar.tax.income.ar_income_tax_helpers import ( + ar_main_income_tax, +) class ar_uses_low_income_tax_tables(Variable): @@ -17,10 +20,7 @@ def formula(tax_unit, period, parameters): p = parameters(period).gov.states.ar.tax.income.rates.main person = tax_unit.members taxable_income = person("ar_taxable_income_joint", period) - main_rate = p.rate.calc(taxable_income) - pre_reduction_tax = main_rate * taxable_income - reduction = p.reduction.calc(taxable_income) - total_main_rate_person = max_(pre_reduction_tax - reduction, 0) + total_main_rate_person = max_(ar_main_income_tax(taxable_income, p), 0) total_main_rate = tax_unit.sum(total_main_rate_person) low_income_tax = add(tax_unit, period, ["ar_low_income_tax_joint"]) return total_main_rate > low_income_tax