From 96aaa069c89cfd63a3d7f6ecc5829165fe317d13 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 25 Sep 2026 13:58:05 -0400 Subject: [PATCH 1/3] Derive Arkansas 2026+ minus adjustments from the indexed statutory tables reduction.yaml had no uprating, so from 2027 its thresholds and amounts stayed at 2026 values while rate.yaml's thresholds uprated, and tax fell by up to $111.98 as income crossed a 2026 bound. From 2026 (no DFA Indexed Tax Brackets card yet), compute the regular tax from A.C.A. 26-51-201(a)(4) as amended by Act 1 of 2026: table (A) at marginal rates up to the high-income threshold; above it, table (B) less the (C) bracket adjustment. The threshold, the (B) 2% row top and each (C) row bound are new parameters indexed like the rate thresholds. The reduction is derived from the same tables, so it moves with them (and with reforms). 2021-2025 keep the published-card schedule behind a new use_published_reduction switch; 2026 results are unchanged. 26-51-201(d)(1) indexes bracket amounts "rounding to the nearest one hundred dollars ($100)", so rounding moves from downwards to nearest. The (C) dollar amounts stay fixed, since (d) indexes only bracket bounds. Co-Authored-By: Claude Opus 5.5 --- .../fix-ar-reduction-uprating.fixed.md | 1 + .../main/high_income/bracket_adjustment.yaml | 349 ++++++++++++++++++ .../income/rates/main/high_income/rate.yaml | 29 ++ .../rates/main/high_income/threshold.yaml | 17 + .../states/ar/tax/income/rates/main/rate.yaml | 26 +- .../ar/tax/income/rates/main/reduction.yaml | 98 +---- .../rates/main/use_published_reduction.yaml | 15 + .../core/test_state_uprating_rounding.py | 23 +- ...x_before_non_refundable_credits_indiv.yaml | 130 +++++++ ...x_before_non_refundable_credits_joint.yaml | 12 + .../tests/test_ar_income_tax_schedule.py | 250 +++++++++++-- ...tax_before_non_refundable_credits_indiv.py | 8 +- ...tax_before_non_refundable_credits_joint.py | 9 +- .../ar/tax/income/ar_income_tax_helpers.py | 48 +++ .../income/ar_uses_low_income_tax_tables.py | 8 +- 15 files changed, 869 insertions(+), 154 deletions(-) create mode 100644 changelog.d/fix-ar-reduction-uprating.fixed.md create mode 100644 policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/bracket_adjustment.yaml create mode 100644 policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/rate.yaml create mode 100644 policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/threshold.yaml create mode 100644 policyengine_us/parameters/gov/states/ar/tax/income/rates/main/use_published_reduction.yaml create mode 100644 policyengine_us/variables/gov/states/ar/tax/income/ar_income_tax_helpers.py 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..3c392d6e2cb --- /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. Bracket bounds, the $94,700 high-income threshold, the (B) 2% row top and each (C) row bound are now indexed and rounded to the nearest $100, per A.C.A. § 26-51-201(d)(1), instead of rounded down. 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..c6deb886c30 --- /dev/null +++ b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/rate.yaml @@ -0,0 +1,29 @@ +description: Arkansas taxes net income above the high-income threshold at these marginal rates, before 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..a66e6b7961e 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 @@ -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..8f45d67fcec --- /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: + 2014-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..8bfb7d0df39 100644 --- a/policyengine_us/tests/core/test_state_uprating_rounding.py +++ b/policyengine_us/tests/core/test_state_uprating_rounding.py @@ -11,17 +11,30 @@ 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 +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)". 5,600 x 1.0298 = 5,766.9 -> 5,800. + main = SYSTEM.parameters.gov.states.ar.tax.income.rates.main + scale = main.rate assert _thresholds(scale, "2027-01-01", (1, 2, 3, 4)) == ( - 5_700, + 5_800, 11_500, - 16_400, - 27_100, + 16_500, + 27_200, ) assert isinf(scale.brackets[5].threshold("2027-01-01")) assert isinf(scale.brackets[6].threshold("2027-01-01")) + high_income = main.high_income + assert high_income.threshold("2027-01-01") == 97_500 + assert high_income.rate.brackets[1].threshold("2027-01-01") == 4_800 + ladder = high_income.bracket_adjustment.brackets + assert _thresholds(high_income.bracket_adjustment, "2027-01-01", (0, 1, 29)) == ( + 97_500, + 97_600, + 100_500, + ) + assert len(ladder) == 30 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..285053bdad5 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,133 @@ # 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 + +- 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 - first bracket adjustment row + period: 2027 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 97_501 + state_code: AR + output: + # 4,800 x 2% + (97,501 - 4,800) x 3.7% - 290 = 3,235.94; the 2% row top is 4,700 x 1.0298 = 4,840, nearest $100 + ar_income_tax_before_non_refundable_credits_indiv: 3_235.94 + +- name: AR 2027 indexed Act 1 of 2026 tables - second bracket adjustment row + period: 2027 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 97_601 + state_code: AR + output: + # 96 + (97,601 - 4,800) x 3.7% - 280 = 3,249.64; the row bound is 94,800 x 1.0298 = 97,625, nearest $100 + ar_income_tax_before_non_refundable_credits_indiv: 3_249.64 + +- name: AR 2027 indexed Act 1 of 2026 tables - top of the last bracket adjustment row + period: 2027 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 100_500 + state_code: AR + output: + # 96 + (100,500 - 4,800) x 3.7% - 10 = 3,626.90; the last bound is 97,600 x 1.0298 = 100,509, nearest $100 + ar_income_tax_before_non_refundable_credits_indiv: 3_626.90 + +- name: AR 2027 indexed Act 1 of 2026 tables - above the bracket adjustment rows + period: 2027 + absolute_error_margin: 0.01 + input: + ar_taxable_income_indiv: 100_501 + state_code: AR + output: + # 96 + (100,501 - 4,800) x 3.7% = 3,636.94 + ar_income_tax_before_non_refundable_credits_indiv: 3_636.94 + +- 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..3127b6ea24a 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,46 @@ 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 the schedule + continuous, 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 +167,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 +226,51 @@ 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 + + # {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 +280,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_schedule_continuous(): + # 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..873bc39d27b --- /dev/null +++ b/policyengine_us/variables/gov/states/ar/tax/income/ar_income_tax_helpers.py @@ -0,0 +1,48 @@ +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, + - (C) less the fixed bracket adjustment for the row the income falls in. + Deriving the reduction from the indexed tables keeps tax continuous at every + indexed bound; a stored reduction would not move with the thresholds. + """ + 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 From cc053d30ff9aee2fb297b7bb330390763b434d1e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 27 Sep 2026 01:52:46 -0400 Subject: [PATCH 2/3] Tighten AR schedule docs and make projected tests robust to CPI refreshes - Helper docstring: marginal taxing keeps tax continuous at every (A) and (B) row bound; the statute's (A)/(B)-(C) seam and (C) row steps remain. - (B) and rate-table descriptions say which income each rate applies to. - Changelog: the threshold, (B) top and (C) bounds were not indexed before (not rounded down); the rounding change also moves withholding's bounds. - Drop the four 2027 YAML cases next to the indexed threshold, which sit within ~0.03% of a nearest-$100 flip; the parameter-relative differential and ladder tests cover them. - The AR rounding regression test now derives expected bounds from the loaded index for 2027-2035 and asserts they differ from rounding down. Co-Authored-By: Claude Opus 5.5 --- .../fix-ar-reduction-uprating.fixed.md | 2 +- .../income/rates/main/high_income/rate.yaml | 2 +- .../states/ar/tax/income/rates/main/rate.yaml | 2 +- .../core/test_state_uprating_rounding.py | 50 +++++++++++-------- ...x_before_non_refundable_credits_indiv.yaml | 44 ++-------------- .../tests/test_ar_income_tax_schedule.py | 7 +-- .../ar/tax/income/ar_income_tax_helpers.py | 9 ++-- 7 files changed, 47 insertions(+), 69 deletions(-) diff --git a/changelog.d/fix-ar-reduction-uprating.fixed.md b/changelog.d/fix-ar-reduction-uprating.fixed.md index 3c392d6e2cb..d2f966a8a12 100644 --- a/changelog.d/fix-ar-reduction-uprating.fixed.md +++ b/changelog.d/fix-ar-reduction-uprating.fixed.md @@ -1 +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. Bracket bounds, the $94,700 high-income threshold, the (B) 2% row top and each (C) row bound are now indexed and rounded to the nearest $100, per A.C.A. § 26-51-201(d)(1), instead of rounded down. The (C) dollar amounts stay fixed. +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/rate.yaml b/policyengine_us/parameters/gov/states/ar/tax/income/rates/main/high_income/rate.yaml index c6deb886c30..643edacf349 100644 --- 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 @@ -1,4 +1,4 @@ -description: Arkansas taxes net income above the high-income threshold at these marginal rates, before the bracket adjustment. +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 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 a66e6b7961e..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 diff --git a/policyengine_us/tests/core/test_state_uprating_rounding.py b/policyengine_us/tests/core/test_state_uprating_rounding.py index 8bfb7d0df39..083c61f6dad 100644 --- a/policyengine_us/tests/core/test_state_uprating_rounding.py +++ b/policyengine_us/tests/core/test_state_uprating_rounding.py @@ -13,28 +13,38 @@ def _thresholds(scale, period, bracket_indexes=(1, 2, 3)): 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)". 5,600 x 1.0298 = 5,766.9 -> 5,800. + # 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 - scale = main.rate - - assert _thresholds(scale, "2027-01-01", (1, 2, 3, 4)) == ( - 5_800, - 11_500, - 16_500, - 27_200, - ) - assert isinf(scale.brackets[5].threshold("2027-01-01")) - assert isinf(scale.brackets[6].threshold("2027-01-01")) + uprating = SYSTEM.parameters.gov.irs.uprating high_income = main.high_income - assert high_income.threshold("2027-01-01") == 97_500 - assert high_income.rate.brackets[1].threshold("2027-01-01") == 4_800 - ladder = high_income.bracket_adjustment.brackets - assert _thresholds(high_income.bracket_adjustment, "2027-01-01", (0, 1, 29)) == ( - 97_500, - 97_600, - 100_500, - ) - assert len(ladder) == 30 + 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 285053bdad5..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 @@ -495,6 +495,10 @@ # 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 @@ -575,46 +579,6 @@ # 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 - first bracket adjustment row - period: 2027 - absolute_error_margin: 0.01 - input: - ar_taxable_income_indiv: 97_501 - state_code: AR - output: - # 4,800 x 2% + (97,501 - 4,800) x 3.7% - 290 = 3,235.94; the 2% row top is 4,700 x 1.0298 = 4,840, nearest $100 - ar_income_tax_before_non_refundable_credits_indiv: 3_235.94 - -- name: AR 2027 indexed Act 1 of 2026 tables - second bracket adjustment row - period: 2027 - absolute_error_margin: 0.01 - input: - ar_taxable_income_indiv: 97_601 - state_code: AR - output: - # 96 + (97,601 - 4,800) x 3.7% - 280 = 3,249.64; the row bound is 94,800 x 1.0298 = 97,625, nearest $100 - ar_income_tax_before_non_refundable_credits_indiv: 3_249.64 - -- name: AR 2027 indexed Act 1 of 2026 tables - top of the last bracket adjustment row - period: 2027 - absolute_error_margin: 0.01 - input: - ar_taxable_income_indiv: 100_500 - state_code: AR - output: - # 96 + (100,500 - 4,800) x 3.7% - 10 = 3,626.90; the last bound is 97,600 x 1.0298 = 100,509, nearest $100 - ar_income_tax_before_non_refundable_credits_indiv: 3_626.90 - -- name: AR 2027 indexed Act 1 of 2026 tables - above the bracket adjustment rows - period: 2027 - absolute_error_margin: 0.01 - input: - ar_taxable_income_indiv: 100_501 - state_code: AR - output: - # 96 + (100,501 - 4,800) x 3.7% = 3,636.94 - ar_income_tax_before_non_refundable_credits_indiv: 3_636.94 - - name: AR 2027 indexed Act 1 of 2026 tables - high income period: 2027 absolute_error_margin: 0.01 diff --git a/policyengine_us/tests/test_ar_income_tax_schedule.py b/policyengine_us/tests/test_ar_income_tax_schedule.py index 3127b6ea24a..551536c25fb 100644 --- a/policyengine_us/tests/test_ar_income_tax_schedule.py +++ b/policyengine_us/tests/test_ar_income_tax_schedule.py @@ -43,8 +43,9 @@ - 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 the schedule - continuous, because the reduction is derived rather than stored. +- 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 @@ -361,7 +362,7 @@ def test_ar_derived_schedule_properties_on_random_tables(seed): assert (change[within_regime] >= -1e-9).all() -def test_ar_rate_table_reform_keeps_projected_schedule_continuous(): +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() 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 index 873bc39d27b..d03274be3c8 100644 --- 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 @@ -14,10 +14,13 @@ def ar_main_income_tax(taxable_income, p): 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, + - (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. - Deriving the reduction from the indexed tables keeps tax continuous at every - indexed bound; a stored reduction would not move with the thresholds. + 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) From e00c01eba779ae44640659931522a999a55b32b0 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 27 Sep 2026 01:59:19 -0400 Subject: [PATCH 3/3] Start use_published_reduction at 0000-01-01 so pre-2014 years still compute The switch started in 2014, so for earlier years it was undefined and the AR tax variables raised ParameterNotFoundError, where main returned $0. Independent review flagged the risk; reproduced for 2012 and 2013 and pinned with a test. Co-Authored-By: Claude Opus 5.5 --- .../ar/tax/income/rates/main/use_published_reduction.yaml | 2 +- policyengine_us/tests/test_ar_income_tax_schedule.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) 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 index 8f45d67fcec..cf6afbdae0a 100644 --- 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 @@ -1,6 +1,6 @@ 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: - 2014-01-01: true + 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 diff --git a/policyengine_us/tests/test_ar_income_tax_schedule.py b/policyengine_us/tests/test_ar_income_tax_schedule.py index 551536c25fb..4e865b8574d 100644 --- a/policyengine_us/tests/test_ar_income_tax_schedule.py +++ b/policyengine_us/tests/test_ar_income_tax_schedule.py @@ -240,6 +240,14 @@ def test_ar_published_reduction_applies_through_2025_only(): ] == [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}}