From 81a3e40e692e1fbd4f2ac0cb87be8aeb823c6114 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 18:05:15 -0400 Subject: [PATCH 1/7] Test the additional_tax_bracket reform with its added bracket unset With its shipped parameters, brackets 7 and 8 of the reform's schedule have infinite thresholds, and its income_tax_main_rates and regular_tax_before_credits are NaN for every household. Three YAML cases pin the finite tax the reform should give (2026 joint and zero-income filers, and the 2025 joint rate table from Rev. Proc. 2024-40, section 2.01). Property tests check tax_at_main_rates for any thresholds, finite or infinite, against policyengine-core's MarginalRateTaxScale and against the old clip form wherever it was defined. A reform-level property checks that, for any thresholds of brackets 7 and 8, the reform's taxes are finite and at least the taxes with the bracket removed. On main, the three YAML cases and seven of the eight Python tests fail with NaN. The check that the baseline schedule is unchanged passes. Co-Authored-By: Claude Opus 5.5 --- .../additional_tax_bracket_reform.yaml | 52 ++ ...st_additional_tax_bracket_unset_bracket.py | 445 ++++++++++++++++++ 2 files changed, 497 insertions(+) create mode 100644 policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py diff --git a/policyengine_us/tests/policy/contrib/additional_tax_bracket/additional_tax_bracket_reform.yaml b/policyengine_us/tests/policy/contrib/additional_tax_bracket/additional_tax_bracket_reform.yaml index eba39e2b714..45ef33eb892 100644 --- a/policyengine_us/tests/policy/contrib/additional_tax_bracket/additional_tax_bracket_reform.yaml +++ b/policyengine_us/tests/policy/contrib/additional_tax_bracket/additional_tax_bracket_reform.yaml @@ -75,3 +75,55 @@ output: regular_tax_before_credits: 145_893 income_tax_main_rates: 145_893 + +# With its default parameters the reform's added bracket is unset: brackets 7 +# and 8 both have a threshold of infinity for every filing status and year. +# Bracket 8 then runs from infinity to infinity, and the clip form +# clip(x, inf, inf) - inf = inf - inf made every household's tax NaN, +# whatever its income. An unset bracket must add nothing, so the reform's tax +# is the tax at the first seven brackets of its own schedule. +- name: Unset added bracket, default 2026 parameters, joint filer. + period: 2026 + absolute_error_margin: 1 + reforms: policyengine_us.reforms.additional_tax_bracket.additional_tax_bracket_reform.additional_tax_bracket + input: + gov.contrib.additional_tax_bracket.in_effect: true + taxable_income: 1_000_000 + filing_status: JOINT + output: + # The reform's 2026 joint schedule, bracket tops 24,300, 98,600, 199,000, + # 303,250, 541,550 and 611,750, rates 10, 15, 25, 28, 33, 35 and 39.6%: + # 0.10 * 24,300 + 0.15 * 74,300 + 0.25 * 100,400 + 0.28 * 104,250 + # + 0.33 * 238,300 + 0.35 * 70,200 + 0.396 * (1,000,000 - 611,750) + # = 2,430 + 11,145 + 25,100 + 29,190 + 78,639 + 24,570 + 153,747 + # = 324,821. + regular_tax_before_credits: 324_821 + income_tax_main_rates: 324_821 + +- name: Unset added bracket, default 2026 parameters, no taxable income. + period: 2026 + absolute_error_margin: 1 + reforms: policyengine_us.reforms.additional_tax_bracket.additional_tax_bracket_reform.additional_tax_bracket + input: + gov.contrib.additional_tax_bracket.in_effect: true + taxable_income: 0 + filing_status: SINGLE + output: + regular_tax_before_credits: 0 + income_tax_main_rates: 0 + +- name: Unset added bracket, default 2025 parameters, joint filer. + period: 2025 + absolute_error_margin: 1 + reforms: policyengine_us.reforms.additional_tax_bracket.additional_tax_bracket_reform.additional_tax_bracket + input: + gov.contrib.additional_tax_bracket.in_effect: true + taxable_income: 1_000_000 + filing_status: JOINT + output: + # The reform's 2025 brackets are the enacted 2025 brackets, so this is + # the 2025 joint rate table (Rev. Proc. 2024-40, section 2.01, Table 1): + # $202,154.50 plus 37% of the excess over $751,600 + # = 202,154.50 + 0.37 * 248,400 = 294,062.50. + regular_tax_before_credits: 294_062.5 + income_tax_main_rates: 294_062.5 diff --git a/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py b/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py new file mode 100644 index 00000000000..6ec5821bf9b --- /dev/null +++ b/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py @@ -0,0 +1,445 @@ +"""An unset or infinite bracket adds no tax in the additional_tax_bracket reform. + +The reform taxes ordinary income on its own eight-bracket schedule, +``gov.contrib.additional_tax_bracket.bracket``, through the shared loop +``tax_at_main_rates``. Its parameter file sets the thresholds of brackets 7 +and 8 to infinity for every filing status and year, so the added bracket is +unset until a user sets bracket 7's threshold. Bracket 8 then runs from +infinity to infinity. The loop took each bracket's amount as +``amount_between(x, bottom, top)``, which is ``clip(x, bottom, top) - bottom``, +and for that bracket clip(x, inf, inf) - inf = inf - inf = NaN. With its +default parameters the reform therefore gave NaN for ``income_tax_main_rates`` +and ``regular_tax_before_credits`` for every household, including one with no +taxable income. The loop now takes max(0, min(x, top) - bottom), the form +policyengine-core's ``MarginalRateTaxScale.calc`` uses. It is zero for a +bracket whose bottom is infinite and equals the clip form whenever the bottom +is finite. + +Properties of ``tax_at_main_rates`` for any thresholds (finite or infinite, in +any order), rates in [0, 1] and taxable amounts in [0, $10 million]: + +- the tax is finite and non-negative; +- it agrees with policyengine-core's ``MarginalRateTaxScale`` over the same + brackets (a differential test), and bit for bit with the old clip form + wherever that form gave a number, so a schedule with finite bracket bottoms, + such as the baseline's, is unchanged; +- the added bracket changes the tax by exactly (r8 - r7) times the income in + bracket 8. So the tax is at least the tax with the bracket removed, meaning + bracket 8's income taxed at bracket 7's rate, whenever r8 >= r7. When + r8 < r7 the added bracket cuts the tax, as intended; +- with bracket 7's threshold at infinity, as shipped, bracket 8 adds exactly + nothing, whatever its rate or threshold. + +Properties of the reform, run in simulations: + +- with its default parameters, every tax is finite in 2025 and 2026; +- with its first seven brackets set to current law and the added bracket + unset, it reproduces the baseline; +- for any thresholds of brackets 7 and 8, finite or infinite, the reform's + ``income_tax_main_rates`` and ``regular_tax_before_credits`` are finite and + at least their values with the bracket removed when r8 >= r7. + +Households have no foreign earned income exclusion. That path subtracts the +tax on the excluded amount, and only the first two properties cover it. +""" + +import numpy as np +import pytest +from policyengine_core.parameters import ParameterNode +from policyengine_core.reforms import Reform +from policyengine_core.taxscales import MarginalRateTaxScale + +from policyengine_us import CountryTaxBenefitSystem, Simulation +from policyengine_us.reforms.additional_tax_bracket.additional_tax_bracket_reform import ( + additional_tax_bracket, +) +from policyengine_us.system import system as baseline_system +from policyengine_us.variables.gov.irs.tax.federal_income.before_credits.tax_at_main_rates import ( + tax_at_main_rates, +) +from policyengine_us.variables.household.demographic.tax_unit.filing_status import ( + FilingStatus, +) + +# Hypothesis is a dev extra; skip rather than fail collection without it. +hypothesis = pytest.importorskip("hypothesis") +st = pytest.importorskip("hypothesis.strategies") + +INF = float("inf") +STATUSES = [status.name for status in FilingStatus] +N_BRACKETS = 8 +BRACKET = "gov.contrib.additional_tax_bracket.bracket" +YEAR = 2026 +REFORM_TAXES = ["income_tax_main_rates", "regular_tax_before_credits"] +DOWNSTREAM_TAXES = REFORM_TAXES + ["income_tax_before_credits", "income_tax"] +# Simulation variables are float32, about seven significant digits, so +# compare taxes of up to a few million dollars to the dollar. +SIMULATION_TOLERANCE = 1.0 + +# --------------------------------------------------------------------------- +# tax_at_main_rates on drawn schedules +# --------------------------------------------------------------------------- + + +def bracket_node(thresholds, rates, instant="2026-01-01"): + """A bracket parameter node at an instant, shaped like the reform's. + + ``thresholds`` maps each filing status to the thresholds of brackets 1 to + n, and ``rates`` lists the n rates. + """ + data = { + "rates": { + str(i): {"values": {instant: rate}} for i, rate in enumerate(rates, 1) + }, + "thresholds": { + str(i): { + status: {"values": {instant: thresholds[status][i - 1]}} + for status in STATUSES + } + for i in range(1, len(rates) + 1) + }, + } + return ParameterNode("bracket", data=data)(instant) + + +def bracket_bottoms(thresholds): + """Bottoms of brackets 1 to n + 1 for one status: 0, then the running + maximum of the thresholds, as the loop clamps them (#9084).""" + return np.maximum.accumulate([0.0, *thresholds]) + + +def scale_tax(incomes, statuses, thresholds, rates): + """The same schedule computed by policyengine-core's MarginalRateTaxScale. + + The loop leaves income above the last threshold untaxed, so the scale + ends with a zero-rate bracket there. + """ + tax = np.zeros_like(incomes) + for status in STATUSES: + in_status = statuses == status + if not in_status.any(): + continue + scale = MarginalRateTaxScale() + scale.thresholds = list(bracket_bottoms(thresholds[status])) + scale.rates = [*rates, 0.0] + tax[in_status] = scale.calc(incomes[in_status]) + return tax + + +def clip_form_tax(incomes, filing_status, bracket, n_brackets): + """tax_at_main_rates as it was before this fix, with amount_between's + clip(x, bottom, top) - bottom.""" + tax = 0 + bottom = 0 + with np.errstate(invalid="ignore"): + for i in range(1, n_brackets + 1): + top = np.maximum(bottom, bracket.thresholds[str(i)][filing_status]) + tax = tax + bracket.rates[str(i)] * (np.clip(incomes, bottom, top) - bottom) + bottom = top + return tax + + +threshold_values = st.one_of( + st.just(INF), + st.floats(0, 2_000_000, allow_nan=False, allow_infinity=False), +) +rate_values = st.floats(0, 1, allow_nan=False, allow_infinity=False) + + +@st.composite +def schedules(draw): + thresholds = { + status: draw( + st.lists(threshold_values, min_size=N_BRACKETS, max_size=N_BRACKETS) + ) + for status in STATUSES + } + rates = draw(st.lists(rate_values, min_size=N_BRACKETS, max_size=N_BRACKETS)) + return thresholds, rates + + +@st.composite +def households(draw): + count = draw(st.integers(1, 12)) + incomes = draw( + st.lists( + st.floats(0, 10_000_000, allow_nan=False, allow_infinity=False), + min_size=count, + max_size=count, + ) + ) + statuses = draw(st.lists(st.sampled_from(STATUSES), min_size=count, max_size=count)) + return np.array(incomes), np.array(statuses) + + +# Shaped like the reform's defaults: brackets 7 and 8 at infinity. +UNSET_BRACKET_THRESHOLDS = { + status: [10_000, 40_000, 90_000, 190_000, 240_000, 600_000, INF, INF] + for status in STATUSES +} +UNSET_BRACKET_RATES = [0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.37, 0.37] +SETTINGS = dict(max_examples=300, deadline=None, derandomize=True) + + +@hypothesis.settings(**SETTINGS) +@hypothesis.given(schedules(), households()) +@hypothesis.example( + (UNSET_BRACKET_THRESHOLDS, UNSET_BRACKET_RATES), + (np.array([0.0, 50_000.0, 2_000_000.0]), np.array(["SINGLE", "JOINT", "SEPARATE"])), +) +def test_schedule_tax_is_finite_and_matches_core_tax_scale(schedule, units): + thresholds, rates = schedule + incomes, statuses = units + bracket = bracket_node(thresholds, rates) + filing_status = FilingStatus.encode(statuses) + tax = tax_at_main_rates(incomes, filing_status, bracket) + + assert np.all(np.isfinite(tax)) + assert np.all(tax >= 0) + np.testing.assert_allclose( + tax, scale_tax(incomes, statuses, thresholds, rates), rtol=1e-12, atol=1e-6 + ) + # Where the clip form gave a number it is unchanged, bit for bit. + old = clip_form_tax(incomes, filing_status, bracket, N_BRACKETS) + defined = ~np.isnan(old) + assert np.array_equal(tax[defined], old[defined]) + # And it gave NaN exactly where a bracket's bottom is infinite. + infinite_bottom = np.array( + [np.isinf(bracket_bottoms(thresholds[s])[N_BRACKETS - 1]) for s in statuses] + ) + assert np.array_equal(~defined, infinite_bottom) + + +@hypothesis.settings(**SETTINGS) +@hypothesis.given(schedules(), households()) +def test_added_bracket_changes_tax_by_its_own_income_only(schedule, units): + thresholds, rates = schedule + incomes, statuses = units + filing_status = FilingStatus.encode(statuses) + with_bracket = tax_at_main_rates( + incomes, filing_status, bracket_node(thresholds, rates) + ) + # Removing the added bracket taxes its income at bracket 7's rate. + removed_rates = [*rates[:-1], rates[-2]] + removed = tax_at_main_rates( + incomes, filing_status, bracket_node(thresholds, removed_rates) + ) + bottoms = np.array([bracket_bottoms(thresholds[s]) for s in statuses]) + bracket_8_income = np.maximum( + 0, np.minimum(incomes, bottoms[:, N_BRACKETS]) - bottoms[:, N_BRACKETS - 1] + ) + np.testing.assert_allclose( + with_bracket - removed, + (rates[-1] - rates[-2]) * bracket_8_income, + rtol=1e-9, + atol=1e-6, + ) + if rates[-1] >= rates[-2]: + assert np.all(with_bracket >= removed - 1e-6) + + +@hypothesis.settings(**SETTINGS) +@hypothesis.given(schedules(), households()) +def test_unset_added_bracket_adds_nothing(schedule, units): + thresholds, rates = schedule + incomes, statuses = units + # Bracket 7's threshold at infinity, as shipped. + thresholds = { + status: [*values[: N_BRACKETS - 2], INF, values[-1]] + for status, values in thresholds.items() + } + filing_status = FilingStatus.encode(statuses) + eight = tax_at_main_rates(incomes, filing_status, bracket_node(thresholds, rates)) + seven = tax_at_main_rates( + incomes, + filing_status, + bracket_node( + {status: thresholds[status][:-1] for status in STATUSES}, rates[:-1] + ), + ) + assert np.all(np.isfinite(eight)) + assert np.array_equal(eight, seven) + + +def test_baseline_schedule_is_unchanged_in_every_year(): + # The baseline's top bracket has an infinite threshold but a finite + # bottom, so the clip form was defined; the new form must reproduce it. + incomes = np.linspace(0, 2_000_000, 401) + for status in STATUSES: + statuses = np.full(incomes.shape, status) + filing_status = FilingStatus.encode(statuses) + for year in range(2018, 2036): + bracket = baseline_system.parameters(f"{year}-01-01").gov.irs.income.bracket + n_brackets = len(list(bracket.rates)) + tax = tax_at_main_rates(incomes, filing_status, bracket) + old = clip_form_tax(incomes, filing_status, bracket, n_brackets) + assert np.all(np.isfinite(tax)) + assert np.array_equal(tax, old), (status, year) + + +# --------------------------------------------------------------------------- +# The reform in simulations +# --------------------------------------------------------------------------- + +ORDINARY_INCOMES = [0, 30_000, 250_000, 700_000, 3_000_000] +# (long-term capital gains, qualified dividends): none, and enough to take +# the Schedule D Tax Worksheet path in regular_tax_before_credits. +PREFERENTIAL_INCOME = [(0, 0), (150_000, 20_000)] + + +def grid_situation(year): + """One single-member tax unit per (status, taxable income, gains) cell. + + Taxable income is an input, so the schedule is the only part of the tax + that varies across these simulations. + """ + situation = {"people": {}, "tax_units": {}, "households": {}} + for status in STATUSES: + for income in ORDINARY_INCOMES: + for gains, dividends in PREFERENTIAL_INCOME: + key = f"{status}_{income}_{gains}" + situation["people"][key] = { + "age": {year: 40}, + "long_term_capital_gains": {year: gains}, + "qualified_dividend_income": {year: dividends}, + } + situation["tax_units"][key] = { + "members": [key], + "filing_status": {year: status}, + "taxable_income": {year: income + gains + dividends}, + } + situation["households"][key] = { + "members": [key], + "state_code": {year: "TX"}, + } + return situation + + +GRID_SIZE = len(STATUSES) * len(ORDINARY_INCOMES) * len(PREFERENTIAL_INCOME) + + +@pytest.fixture(scope="module") +def reform_system(): + """The reform's system, built once; each simulation that changes its + parameters works on its own clone.""" + return CountryTaxBenefitSystem(reform=additional_tax_bracket) + + +def calculate(variables, year, tax_benefit_system, reform=None): + sim = Simulation( + tax_benefit_system=tax_benefit_system, + reform=reform, + situation=grid_situation(year), + ) + assert np.array_equal( + sim.calculate("filing_status", year).decode_to_str(), + np.repeat(STATUSES, GRID_SIZE // len(STATUSES)), + ) + return {variable: sim.calculate(variable, year) for variable in variables} + + +@pytest.mark.parametrize("year", [2025, 2026]) +def test_default_parameters_give_finite_taxes(reform_system, year): + taxes = calculate(DOWNSTREAM_TAXES, year, reform_system) + for variable, values in taxes.items(): + assert len(values) == GRID_SIZE + assert np.all(np.isfinite(values)), variable + assert np.all(taxes["income_tax_main_rates"] >= 0) + assert taxes["income_tax_main_rates"].max() > 0 + + +def current_law_first_seven_brackets(year): + """Set the reform's brackets 1 to 7 to the baseline's for one year, and + leave the added bracket unset (its shipped infinite thresholds).""" + p = baseline_system.parameters(f"{year}-01-01").gov.irs.income.bracket + period = f"{year}-01-01.{year}-12-31" + changes = {} + for i in range(1, 8): + changes[f"{BRACKET}.rates.{i}"] = {period: float(p.rates[str(i)])} + for status in STATUSES: + changes[f"{BRACKET}.thresholds.{i}.{status}"] = { + period: float(p.thresholds[str(i)][status]) + } + return Reform.from_dict(changes, country_id="us") + + +def test_reform_with_unset_bracket_reproduces_current_law(reform_system): + reform = calculate( + DOWNSTREAM_TAXES, + YEAR, + reform_system, + reform=current_law_first_seven_brackets(YEAR), + ) + baseline = calculate(DOWNSTREAM_TAXES, YEAR, baseline_system) + for variable in DOWNSTREAM_TAXES: + np.testing.assert_allclose( + reform[variable], baseline[variable], atol=SIMULATION_TOLERANCE + ) + assert baseline["income_tax_main_rates"].max() > 0 + + +def added_bracket(thresholds_7, thresholds_8, rate_7, rate_8): + period = f"{YEAR}-01-01.{YEAR}-12-31" + changes = { + f"{BRACKET}.rates.7": {period: rate_7}, + f"{BRACKET}.rates.8": {period: rate_8}, + } + for status in STATUSES: + changes[f"{BRACKET}.thresholds.7.{status}"] = {period: thresholds_7[status]} + changes[f"{BRACKET}.thresholds.8.{status}"] = {period: thresholds_8[status]} + return Reform.from_dict(changes, country_id="us") + + +def per_status(values): + return st.fixed_dictionaries({status: values for status in STATUSES}) + + +simulation_thresholds = st.one_of( + st.just(INF), + st.floats(0, 4_000_000, allow_nan=False, allow_infinity=False), +) + + +def uniform(value): + return {status: value for status in STATUSES} + + +@hypothesis.settings( + max_examples=12, + deadline=None, + derandomize=True, +) +@hypothesis.given( + per_status(simulation_thresholds), + per_status(simulation_thresholds), + st.tuples(rate_values, rate_values).map(sorted), +) +# The shipped parameters: both thresholds infinite. +@hypothesis.example(uniform(INF), uniform(INF), [0.396, 0.396]) +# The YAML tests' use: a finite bracket 7 threshold, bracket 8 unbounded. +@hypothesis.example(uniform(800_000), uniform(INF), [0.396, 0.42]) +# Bracket 8's threshold below bracket 7's, and a zero bracket 7 threshold. +@hypothesis.example(uniform(900_000), uniform(500_000), [0.3, 0.5]) +@hypothesis.example(uniform(0), uniform(INF), [0.0, 1.0]) +def test_reform_tax_is_finite_and_at_least_tax_without_the_bracket( + reform_system, thresholds_7, thresholds_8, rates +): + rate_7, rate_8 = rates + with_bracket = calculate( + REFORM_TAXES, + YEAR, + reform_system, + reform=added_bracket(thresholds_7, thresholds_8, rate_7, rate_8), + ) + removed = calculate( + REFORM_TAXES, + YEAR, + reform_system, + reform=added_bracket(thresholds_7, thresholds_8, rate_7, rate_7), + ) + for variable in REFORM_TAXES: + assert np.all(np.isfinite(with_bracket[variable])), variable + assert np.all(np.isfinite(removed[variable])), variable + assert np.all( + with_bracket[variable] >= removed[variable] - SIMULATION_TOLERANCE + ), variable From 692e0dab9dd46455d31f75a366ed3677f47146a6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 18:05:25 -0400 Subject: [PATCH 2/7] Give an unset or infinite tax bracket zero tax instead of NaN tax_at_main_rates took each bracket's amount as amount_between's clip(x, bottom, top) - bottom. Above an infinite threshold, a bracket runs from infinity to infinity and that is inf - inf = NaN, so the additional_tax_bracket reform, whose brackets 7 and 8 ship with infinite thresholds, gave NaN for every household. The loop now takes max(0, min(x, top) - bottom), as policyengine-core's MarginalRateTaxScale.calc does. That is zero for such a bracket and equals the clip form bit for bit whenever the bottom is finite, so the baseline schedule is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../fix-additional-tax-bracket-nan.fixed.md | 1 + .../before_credits/tax_at_main_rates.py | 24 ++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 changelog.d/fix-additional-tax-bracket-nan.fixed.md diff --git a/changelog.d/fix-additional-tax-bracket-nan.fixed.md b/changelog.d/fix-additional-tax-bracket-nan.fixed.md new file mode 100644 index 00000000000..eab4b840e8b --- /dev/null +++ b/changelog.d/fix-additional-tax-bracket-nan.fixed.md @@ -0,0 +1 @@ +Give the additional_tax_bracket contrib reform a finite tax while its added bracket is unset. With the shipped infinite thresholds for brackets 7 and 8, income_tax_main_rates and regular_tax_before_credits were NaN for every household. diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/before_credits/tax_at_main_rates.py b/policyengine_us/variables/gov/irs/tax/federal_income/before_credits/tax_at_main_rates.py index ccdad76e0d8..7ca33f01037 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/before_credits/tax_at_main_rates.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/before_credits/tax_at_main_rates.py @@ -6,20 +6,26 @@ def tax_at_main_rates(taxable_amount, filing_status, bracket): `bracket` is a bracket parameter node with `rates` and `thresholds`, such as `parameters(period).gov.irs.income.bracket` or a contrib - reform's replacement schedule. + reform's replacement schedule. A threshold of infinity leaves every + bracket above it empty, so those brackets add nothing. """ tax = 0 bracket_bottom = 0 for i in range(1, len(list(bracket.rates.__iter__())) + 1): b = str(i) - # Clamp to the running lower bound: with a non-monotone threshold - # configuration, amount_between's clip returns the upper bound for - # every input, so an inverted bracket would add - # rate * (top - bottom) < 0 to every filer of the status at any - # income (#9084). + # Clamp to the running lower bound so brackets never overlap: with + # a non-monotone threshold configuration, an inverted bracket is + # empty and the next one starts at the highest threshold so far + # (#9084). bracket_top = max_(bracket_bottom, bracket.thresholds[b][filing_status]) - tax += bracket.rates[b] * amount_between( - taxable_amount, bracket_bottom, bracket_top - ) + # max(0, min(x, top) - bottom), the form policyengine-core's + # MarginalRateTaxScale.calc uses, equals amount_between's + # clip(x, bottom, top) - bottom whenever the bottom is finite. A + # bracket above an infinite threshold, such as the + # additional_tax_bracket reform's unset bracket 8, runs from infinity + # to infinity: there the clip form is inf - inf = NaN, and this form + # is zero. + amount_in_bracket = max_(0, min_(taxable_amount, bracket_top) - bracket_bottom) + tax += bracket.rates[b] * amount_in_bracket bracket_bottom = bracket_top return tax From dd18085a25b904297ce07a4c6937834818c68e8d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 19:01:06 -0400 Subject: [PATCH 3/7] Answer review: health checks, a cheaper reform property, accurate notes - Suppress Hypothesis's too_slow and data_too_large health checks, as the repository's other Hypothesis tests do: on a loaded machine one draw stalled for 180 seconds and failed the reform-level property. - Each reform-level example runs two simulations on clones of the reform's system, and the file runs in both the contrib YAML step and the contrib Python step. Set brackets 7 and 8 per filing status so that one simulation covers five threshold shapes, and run three explicit and three generated examples instead of four and twelve. - Say that no simulation sets a foreign earned income exclusion, so the reform's subtraction of the tax on the excluded amount is covered only through the schedule properties. - Note that the reform's own 2026 schedule is not current law, and that the fix also covers an infinite threshold below the baseline's top bracket. Co-Authored-By: Claude Opus 5.5 --- .../fix-additional-tax-bracket-nan.fixed.md | 2 +- .../additional_tax_bracket_reform.yaml | 6 ++- ...st_additional_tax_bracket_unset_bracket.py | 53 ++++++++++++++----- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/changelog.d/fix-additional-tax-bracket-nan.fixed.md b/changelog.d/fix-additional-tax-bracket-nan.fixed.md index eab4b840e8b..a037f94460b 100644 --- a/changelog.d/fix-additional-tax-bracket-nan.fixed.md +++ b/changelog.d/fix-additional-tax-bracket-nan.fixed.md @@ -1 +1 @@ -Give the additional_tax_bracket contrib reform a finite tax while its added bracket is unset. With the shipped infinite thresholds for brackets 7 and 8, income_tax_main_rates and regular_tax_before_credits were NaN for every household. +Give a tax bracket above an infinite threshold zero tax instead of NaN. The additional_tax_bracket contrib reform ships brackets 7 and 8 with infinite thresholds, so until a user set bracket 7 its income_tax_main_rates and regular_tax_before_credits were NaN for every household; a reform that set any of the baseline schedule's first six thresholds to infinity hit the same NaN. diff --git a/policyengine_us/tests/policy/contrib/additional_tax_bracket/additional_tax_bracket_reform.yaml b/policyengine_us/tests/policy/contrib/additional_tax_bracket/additional_tax_bracket_reform.yaml index 45ef33eb892..bfcee1e79da 100644 --- a/policyengine_us/tests/policy/contrib/additional_tax_bracket/additional_tax_bracket_reform.yaml +++ b/policyengine_us/tests/policy/contrib/additional_tax_bracket/additional_tax_bracket_reform.yaml @@ -91,8 +91,10 @@ taxable_income: 1_000_000 filing_status: JOINT output: - # The reform's 2026 joint schedule, bracket tops 24,300, 98,600, 199,000, - # 303,250, 541,550 and 611,750, rates 10, 15, 25, 28, 33, 35 and 39.6%: + # The reform's own 2026 joint schedule, bracket tops 24,300, 98,600, + # 199,000, 303,250, 541,550 and 611,750, rates 10, 15, 25, 28, 33, 35 and + # 39.6%. Its parameter file sets these from 2026; they are not current + # law, whose 2026 rates are 10, 12, 22, 24, 32, 35 and 37%: # 0.10 * 24,300 + 0.15 * 74,300 + 0.25 * 100,400 + 0.28 * 104,250 # + 0.33 * 238,300 + 0.35 * 70,200 + 0.396 * (1,000,000 - 611,750) # = 2,430 + 11,145 + 25,100 + 29,190 + 78,639 + 24,570 + 153,747 diff --git a/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py b/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py index 6ec5821bf9b..b0560931fc0 100644 --- a/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py +++ b/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py @@ -39,8 +39,10 @@ ``income_tax_main_rates`` and ``regular_tax_before_credits`` are finite and at least their values with the bracket removed when r8 >= r7. -Households have no foreign earned income exclusion. That path subtracts the -tax on the excluded amount, and only the first two properties cover it. +No simulation here sets a foreign earned income exclusion, so the reform's +subtraction of the tax on the excluded amount is covered only through the +properties of ``tax_at_main_rates``, which hold for the excluded amount as for +any other, and not end to end. """ import numpy as np @@ -178,7 +180,15 @@ def households(draw): for status in STATUSES } UNSET_BRACKET_RATES = [0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.37, 0.37] -SETTINGS = dict(max_examples=300, deadline=None, derandomize=True) +# A loaded machine can stall a draw; as in the repository's other Hypothesis +# tests, slow generation is not a failure. +HEALTH_CHECKS = [hypothesis.HealthCheck.too_slow, hypothesis.HealthCheck.data_too_large] +SETTINGS = dict( + max_examples=300, + deadline=None, + derandomize=True, + suppress_health_check=HEALTH_CHECKS, +) @hypothesis.settings(**SETTINGS) @@ -400,27 +410,44 @@ def per_status(values): ) -def uniform(value): - return {status: value for status in STATUSES} +# Thresholds are set per filing status, so one simulation covers a shape of +# brackets 7 and 8 for each status: (bracket 7 threshold, bracket 8 threshold). +THRESHOLD_SHAPES = { + # As shipped: both infinite. + "SINGLE": (INF, INF), + # As the YAML tests use it: a finite bracket 7 threshold, bracket 8 + # unbounded. + "JOINT": (800_000, INF), + # Bracket 8's threshold below bracket 7's. + "SEPARATE": (900_000, 500_000), + # A zero bracket 7 threshold. + "HEAD_OF_HOUSEHOLD": (0, INF), + # Bracket 7 unbounded above a finite bracket 8 threshold. + "SURVIVING_SPOUSE": (INF, 300_000), +} +SHAPES_7 = {status: shape[0] for status, shape in THRESHOLD_SHAPES.items()} +SHAPES_8 = {status: shape[1] for status, shape in THRESHOLD_SHAPES.items()} +# Each example runs two simulations on clones of the reform's system, and the +# file runs in two CI steps, so the generated examples are few; the schedule +# properties above take 300 examples each. @hypothesis.settings( - max_examples=12, + max_examples=3, deadline=None, derandomize=True, + suppress_health_check=HEALTH_CHECKS, ) @hypothesis.given( per_status(simulation_thresholds), per_status(simulation_thresholds), st.tuples(rate_values, rate_values).map(sorted), ) -# The shipped parameters: both thresholds infinite. -@hypothesis.example(uniform(INF), uniform(INF), [0.396, 0.396]) -# The YAML tests' use: a finite bracket 7 threshold, bracket 8 unbounded. -@hypothesis.example(uniform(800_000), uniform(INF), [0.396, 0.42]) -# Bracket 8's threshold below bracket 7's, and a zero bracket 7 threshold. -@hypothesis.example(uniform(900_000), uniform(500_000), [0.3, 0.5]) -@hypothesis.example(uniform(0), uniform(INF), [0.0, 1.0]) +# The shipped rates, equal, so the bracket changes nothing. +@hypothesis.example(SHAPES_7, SHAPES_8, [0.396, 0.396]) +# A higher top rate, as in the YAML tests. +@hypothesis.example(SHAPES_7, SHAPES_8, [0.396, 0.42]) +@hypothesis.example(SHAPES_7, SHAPES_8, [0.0, 1.0]) def test_reform_tax_is_finite_and_at_least_tax_without_the_bracket( reform_system, thresholds_7, thresholds_8, rates ): From af420d432d72aa237b30a7746f42764369538b69 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 19:14:55 -0400 Subject: [PATCH 4/7] Share one bracket walk between the tax and the amount below 25 percent #9792 added amount_taxed_below_rate beside tax_at_main_rates. It walks the same clamped brackets but takes each bracket's amount with amount_between's clip form, so a bracket above an infinite threshold whose rate is below the limit still gave inf - inf = NaN: for the additional_tax_bracket reform, a bracket 8 rate under 25 percent with bracket 7 unset. Both functions now take each bracket's amount from amounts_in_brackets, which holds the #9084 clamp and the max(0, min(x, top) - bottom) form, so the amount below a rate stays consistent with the tax by construction. The tests check amount_taxed_below_rate for any thresholds and any rate limit: finite, between zero and the amount, bit for bit the tax at rates of 1 below the limit and 0 otherwise, close to MarginalRateTaxScale at those rates, and bit for bit the old clip form wherever that form gave a number, including on the baseline schedule for every year from 2018 to 2035. The simulation tests also check the reform's taxable_income_taxed_below_25_percent. Co-Authored-By: Claude Opus 5.5 --- .../fix-additional-tax-bracket-nan.fixed.md | 2 +- ...st_additional_tax_bracket_unset_bracket.py | 128 +++++++++++++++--- .../before_credits/tax_at_main_rates.py | 36 ++--- 3 files changed, 127 insertions(+), 39 deletions(-) diff --git a/changelog.d/fix-additional-tax-bracket-nan.fixed.md b/changelog.d/fix-additional-tax-bracket-nan.fixed.md index a037f94460b..95083ce9aff 100644 --- a/changelog.d/fix-additional-tax-bracket-nan.fixed.md +++ b/changelog.d/fix-additional-tax-bracket-nan.fixed.md @@ -1 +1 @@ -Give a tax bracket above an infinite threshold zero tax instead of NaN. The additional_tax_bracket contrib reform ships brackets 7 and 8 with infinite thresholds, so until a user set bracket 7 its income_tax_main_rates and regular_tax_before_credits were NaN for every household; a reform that set any of the baseline schedule's first six thresholds to infinity hit the same NaN. +Give a tax bracket above an infinite threshold zero tax, and zero income taxed below 25 percent, instead of NaN. The additional_tax_bracket contrib reform ships brackets 7 and 8 with infinite thresholds, so until a user set bracket 7 its income_tax_main_rates and regular_tax_before_credits were NaN for every household; a reform that set any of the baseline schedule's first six thresholds to infinity hit the same NaN. diff --git a/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py b/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py index b0560931fc0..3a4afc500f0 100644 --- a/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py +++ b/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py @@ -1,16 +1,18 @@ """An unset or infinite bracket adds no tax in the additional_tax_bracket reform. The reform taxes ordinary income on its own eight-bracket schedule, -``gov.contrib.additional_tax_bracket.bracket``, through the shared loop -``tax_at_main_rates``. Its parameter file sets the thresholds of brackets 7 -and 8 to infinity for every filing status and year, so the added bracket is -unset until a user sets bracket 7's threshold. Bracket 8 then runs from -infinity to infinity. The loop took each bracket's amount as -``amount_between(x, bottom, top)``, which is ``clip(x, bottom, top) - bottom``, -and for that bracket clip(x, inf, inf) - inf = inf - inf = NaN. With its -default parameters the reform therefore gave NaN for ``income_tax_main_rates`` -and ``regular_tax_before_credits`` for every household, including one with no -taxable income. The loop now takes max(0, min(x, top) - bottom), the form +``gov.contrib.additional_tax_bracket.bracket``, through the shared functions +``tax_at_main_rates`` and ``amount_taxed_below_rate``. Its parameter file sets +the thresholds of brackets 7 and 8 to infinity for every filing status and +year, so the added bracket is unset until a user sets bracket 7's threshold. +Bracket 8 then runs from infinity to infinity. Both functions took each +bracket's amount as ``amount_between(x, bottom, top)``, which is +``clip(x, bottom, top) - bottom``, and for that bracket +clip(x, inf, inf) - inf = inf - inf = NaN. With its default parameters the +reform therefore gave NaN for ``income_tax_main_rates`` and +``regular_tax_before_credits`` for every household, including one with no +taxable income. Both now take each bracket's amount from +``amounts_in_brackets``, as max(0, min(x, top) - bottom), the form policyengine-core's ``MarginalRateTaxScale.calc`` uses. It is zero for a bracket whose bottom is infinite and equals the clip form whenever the bottom is finite. @@ -30,14 +32,25 @@ - with bracket 7's threshold at infinity, as shipped, bracket 8 adds exactly nothing, whatever its rate or threshold. +``amount_taxed_below_rate``, under the same conditions and for any rate limit +in [0, 1], is finite, between zero and the amount, bit for bit the tax at +rates of 1 for the brackets taxed below the limit and 0 for the others, and +bit for bit the old clip form wherever that form gave a number. + Properties of the reform, run in simulations: -- with its default parameters, every tax is finite in 2025 and 2026; +- with its default parameters, every tax, and the taxable income taxed + below 25 percent, is finite in 2025 and 2026; - with its first seven brackets set to current law and the added bracket unset, it reproduces the baseline; - for any thresholds of brackets 7 and 8, finite or infinite, the reform's - ``income_tax_main_rates`` and ``regular_tax_before_credits`` are finite and - at least their values with the bracket removed when r8 >= r7. + ``income_tax_main_rates``, ``regular_tax_before_credits`` and + ``taxable_income_taxed_below_25_percent`` are finite, and its two taxes are + at least their values with the bracket removed when r8 >= r7. The + households have no 28 percent rate gain or unrecaptured section 1250 gain, + so the amount taxed below 25 percent does not change which of their income + is taxed at the regular rates (``dwks19`` and + ``capital_gains_excluded_from_taxable_income``). No simulation here sets a foreign earned income exclusion, so the reform's subtraction of the tax on the excluded amount is covered only through the @@ -57,6 +70,7 @@ ) from policyengine_us.system import system as baseline_system from policyengine_us.variables.gov.irs.tax.federal_income.before_credits.tax_at_main_rates import ( + amount_taxed_below_rate, tax_at_main_rates, ) from policyengine_us.variables.household.demographic.tax_unit.filing_status import ( @@ -73,7 +87,9 @@ BRACKET = "gov.contrib.additional_tax_bracket.bracket" YEAR = 2026 REFORM_TAXES = ["income_tax_main_rates", "regular_tax_before_credits"] -DOWNSTREAM_TAXES = REFORM_TAXES + ["income_tax_before_credits", "income_tax"] +# Every variable the reform replaces. +REFORM_VARIABLES = REFORM_TAXES + ["taxable_income_taxed_below_25_percent"] +DOWNSTREAM_VARIABLES = REFORM_VARIABLES + ["income_tax_before_credits", "income_tax"] # Simulation variables are float32, about seven significant digits, so # compare taxes of up to a few million dollars to the dollar. SIMULATION_TOLERANCE = 1.0 @@ -141,6 +157,19 @@ def clip_form_tax(incomes, filing_status, bracket, n_brackets): return tax +def clip_form_amount_below_rate(incomes, filing_status, bracket, n_brackets, limit): + """amount_taxed_below_rate as it was before this fix.""" + amount = 0 + bottom = 0 + with np.errstate(invalid="ignore"): + for i in range(1, n_brackets + 1): + top = np.maximum(bottom, bracket.thresholds[str(i)][filing_status]) + if bracket.rates[str(i)] < limit: + amount = amount + (np.clip(incomes, bottom, top) - bottom) + bottom = top + return amount + + threshold_values = st.one_of( st.just(INF), st.floats(0, 2_000_000, allow_nan=False, allow_infinity=False), @@ -271,9 +300,55 @@ def test_unset_added_bracket_adds_nothing(schedule, units): assert np.array_equal(eight, seven) +@hypothesis.settings(**SETTINGS) +@hypothesis.given(schedules(), households(), rate_values) +@hypothesis.example( + (UNSET_BRACKET_THRESHOLDS, [0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.10, 0.10]), + (np.array([0.0, 50_000.0, 2_000_000.0]), np.array(["SINGLE", "JOINT", "SEPARATE"])), + 0.25, +) +def test_amount_taxed_below_rate_is_finite_and_matches_indicator_rates( + schedule, units, limit +): + thresholds, rates = schedule + incomes, statuses = units + bracket = bracket_node(thresholds, rates) + filing_status = FilingStatus.encode(statuses) + # With no rate below the limit the function returns the scalar 0, which + # policyengine-core fills into an array for a formula. + amount = np.broadcast_to( + amount_taxed_below_rate(incomes, filing_status, bracket, limit), + incomes.shape, + ) + + assert np.all(np.isfinite(amount)) + assert np.all(amount >= 0) + # The brackets' pieces sum to at most the amount, up to rounding. + assert np.all(amount <= incomes + 1e-6) + # The amount is the tax at rates of 1 below the limit and 0 otherwise. + indicators = [1.0 if rate < limit else 0.0 for rate in rates] + at_indicators = tax_at_main_rates( + incomes, filing_status, bracket_node(thresholds, indicators) + ) + assert np.array_equal(amount, at_indicators) + np.testing.assert_allclose( + amount, + scale_tax(incomes, statuses, thresholds, indicators), + rtol=1e-12, + atol=1e-6, + ) + old = np.broadcast_to( + clip_form_amount_below_rate(incomes, filing_status, bracket, N_BRACKETS, limit), + incomes.shape, + ) + defined = ~np.isnan(old) + assert np.array_equal(amount[defined], old[defined]) + + def test_baseline_schedule_is_unchanged_in_every_year(): # The baseline's top bracket has an infinite threshold but a finite - # bottom, so the clip form was defined; the new form must reproduce it. + # bottom, so the clip form was defined; the new form must reproduce it, + # for the tax and for the income taxed below 25 percent. incomes = np.linspace(0, 2_000_000, 401) for status in STATUSES: statuses = np.full(incomes.shape, status) @@ -285,6 +360,14 @@ def test_baseline_schedule_is_unchanged_in_every_year(): old = clip_form_tax(incomes, filing_status, bracket, n_brackets) assert np.all(np.isfinite(tax)) assert np.array_equal(tax, old), (status, year) + limit = baseline_system.parameters( + f"{year}-01-01" + ).gov.irs.capital_gains.regular_rate_limit + below = amount_taxed_below_rate(incomes, filing_status, bracket, limit) + old_below = clip_form_amount_below_rate( + incomes, filing_status, bracket, n_brackets, limit + ) + assert np.array_equal(below, old_below), (status, year) # --------------------------------------------------------------------------- @@ -350,7 +433,7 @@ def calculate(variables, year, tax_benefit_system, reform=None): @pytest.mark.parametrize("year", [2025, 2026]) def test_default_parameters_give_finite_taxes(reform_system, year): - taxes = calculate(DOWNSTREAM_TAXES, year, reform_system) + taxes = calculate(DOWNSTREAM_VARIABLES, year, reform_system) for variable, values in taxes.items(): assert len(values) == GRID_SIZE assert np.all(np.isfinite(values)), variable @@ -375,13 +458,13 @@ def current_law_first_seven_brackets(year): def test_reform_with_unset_bracket_reproduces_current_law(reform_system): reform = calculate( - DOWNSTREAM_TAXES, + DOWNSTREAM_VARIABLES, YEAR, reform_system, reform=current_law_first_seven_brackets(YEAR), ) - baseline = calculate(DOWNSTREAM_TAXES, YEAR, baseline_system) - for variable in DOWNSTREAM_TAXES: + baseline = calculate(DOWNSTREAM_VARIABLES, YEAR, baseline_system) + for variable in DOWNSTREAM_VARIABLES: np.testing.assert_allclose( reform[variable], baseline[variable], atol=SIMULATION_TOLERANCE ) @@ -453,20 +536,21 @@ def test_reform_tax_is_finite_and_at_least_tax_without_the_bracket( ): rate_7, rate_8 = rates with_bracket = calculate( - REFORM_TAXES, + REFORM_VARIABLES, YEAR, reform_system, reform=added_bracket(thresholds_7, thresholds_8, rate_7, rate_8), ) removed = calculate( - REFORM_TAXES, + REFORM_VARIABLES, YEAR, reform_system, reform=added_bracket(thresholds_7, thresholds_8, rate_7, rate_7), ) - for variable in REFORM_TAXES: + for variable in REFORM_VARIABLES: assert np.all(np.isfinite(with_bracket[variable])), variable assert np.all(np.isfinite(removed[variable])), variable + for variable in REFORM_TAXES: assert np.all( with_bracket[variable] >= removed[variable] - SIMULATION_TOLERANCE ), variable diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/before_credits/tax_at_main_rates.py b/policyengine_us/variables/gov/irs/tax/federal_income/before_credits/tax_at_main_rates.py index 94df16eb8cd..d414c735966 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/before_credits/tax_at_main_rates.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/before_credits/tax_at_main_rates.py @@ -1,15 +1,14 @@ from policyengine_us.model_api import * -def tax_at_main_rates(taxable_amount, filing_status, bracket): - """Tax on an amount at the ordinary rate schedule of 26 U.S.C. 1. +def amounts_in_brackets(taxable_amount, filing_status, bracket): + """Each bracket's key and the part of an amount that falls in it. `bracket` is a bracket parameter node with `rates` and `thresholds`, such as `parameters(period).gov.irs.income.bracket` or a contrib reform's replacement schedule. A threshold of infinity leaves every - bracket above it empty, so those brackets add nothing. + bracket above it empty. """ - tax = 0 bracket_bottom = 0 for i in range(1, len(list(bracket.rates.__iter__())) + 1): b = str(i) @@ -25,9 +24,18 @@ def tax_at_main_rates(taxable_amount, filing_status, bracket): # additional_tax_bracket reform's unset bracket 8, runs from infinity # to infinity: there the clip form is inf - inf = NaN, and this form # is zero. - amount_in_bracket = max_(0, min_(taxable_amount, bracket_top) - bracket_bottom) - tax += bracket.rates[b] * amount_in_bracket + yield b, max_(0, min_(taxable_amount, bracket_top) - bracket_bottom) bracket_bottom = bracket_top + + +def tax_at_main_rates(taxable_amount, filing_status, bracket): + """Tax on an amount at the ordinary rate schedule of 26 U.S.C. 1. + + `bracket` is a bracket parameter node, as in `amounts_in_brackets`. + """ + tax = 0 + for b, amount in amounts_in_brackets(taxable_amount, filing_status, bracket): + tax += bracket.rates[b] * amount return tax @@ -35,15 +43,11 @@ def amount_taxed_below_rate(taxable_amount, filing_status, bracket, rate): """The part of an amount that the ordinary rate schedule taxes below a rate. For example, 26 U.S.C. 1(h)(1)(A)(ii)(I) refers to "the amount of taxable - income taxed at a rate below 25 percent". Brackets are walked and clamped - as in tax_at_main_rates, so the amount is consistent with the tax. + income taxed at a rate below 25 percent". The brackets are those of + tax_at_main_rates, so the amount is consistent with the tax. """ - amount = 0 - bracket_bottom = 0 - for i in range(1, len(list(bracket.rates.__iter__())) + 1): - b = str(i) - bracket_top = max_(bracket_bottom, bracket.thresholds[b][filing_status]) + amount_below_rate = 0 + for b, amount in amounts_in_brackets(taxable_amount, filing_status, bracket): if bracket.rates[b] < rate: - amount += amount_between(taxable_amount, bracket_bottom, bracket_top) - bracket_bottom = bracket_top - return amount + amount_below_rate += amount + return amount_below_rate From a42a67842ee970c4d29df0ec04867e73ba350107 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 19:39:54 -0400 Subject: [PATCH 5/7] Pin the 25 percent boundary and tighten wording, from re-review - Add an explicit example where a bracket's rate equals the 25 percent limit, as the reform's 2026 bracket 3 does, so a rate equal to the limit stays outside the amount taxed below it whatever examples Hypothesis generates. - Say that amount_taxed_below_rate took the clip form only for brackets taxed below its limit, and that the brackets' pieces sum to at most the taxable amount. - Reword the changelog: the bracket adds nothing to the tax and to the income taxed below 25 percent; the total amount is not zeroed. Co-Authored-By: Claude Opus 5.5 --- changelog.d/fix-additional-tax-bracket-nan.fixed.md | 2 +- .../test_additional_tax_bracket_unset_bracket.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/changelog.d/fix-additional-tax-bracket-nan.fixed.md b/changelog.d/fix-additional-tax-bracket-nan.fixed.md index 95083ce9aff..372d44f1e1f 100644 --- a/changelog.d/fix-additional-tax-bracket-nan.fixed.md +++ b/changelog.d/fix-additional-tax-bracket-nan.fixed.md @@ -1 +1 @@ -Give a tax bracket above an infinite threshold zero tax, and zero income taxed below 25 percent, instead of NaN. The additional_tax_bracket contrib reform ships brackets 7 and 8 with infinite thresholds, so until a user set bracket 7 its income_tax_main_rates and regular_tax_before_credits were NaN for every household; a reform that set any of the baseline schedule's first six thresholds to infinity hit the same NaN. +A tax bracket above an infinite threshold now adds nothing, instead of NaN, to the tax and to the taxable income taxed below 25 percent. The additional_tax_bracket contrib reform ships brackets 7 and 8 with infinite thresholds, so until a user set bracket 7 its income_tax_main_rates and regular_tax_before_credits were NaN for every household; a reform that set any of the baseline schedule's first six thresholds to infinity hit the same NaN. diff --git a/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py b/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py index 3a4afc500f0..116eac2532b 100644 --- a/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py +++ b/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py @@ -7,7 +7,8 @@ year, so the added bracket is unset until a user sets bracket 7's threshold. Bracket 8 then runs from infinity to infinity. Both functions took each bracket's amount as ``amount_between(x, bottom, top)``, which is -``clip(x, bottom, top) - bottom``, and for that bracket +``clip(x, bottom, top) - bottom`` (``amount_taxed_below_rate`` only for +brackets taxed below its limit), and for that bracket clip(x, inf, inf) - inf = inf - inf = NaN. With its default parameters the reform therefore gave NaN for ``income_tax_main_rates`` and ``regular_tax_before_credits`` for every household, including one with no @@ -307,6 +308,13 @@ def test_unset_added_bracket_adds_nothing(schedule, units): (np.array([0.0, 50_000.0, 2_000_000.0]), np.array(["SINGLE", "JOINT", "SEPARATE"])), 0.25, ) +# A rate equal to the limit is not below it, as with the reform's 2026 +# bracket 3 rate of 25 percent. +@hypothesis.example( + (UNSET_BRACKET_THRESHOLDS, [0.10, 0.15, 0.25, 0.28, 0.33, 0.35, 0.396, 0.396]), + (np.array([0.0, 50_000.0, 2_000_000.0]), np.array(["SINGLE", "JOINT", "SEPARATE"])), + 0.25, +) def test_amount_taxed_below_rate_is_finite_and_matches_indicator_rates( schedule, units, limit ): @@ -323,7 +331,7 @@ def test_amount_taxed_below_rate_is_finite_and_matches_indicator_rates( assert np.all(np.isfinite(amount)) assert np.all(amount >= 0) - # The brackets' pieces sum to at most the amount, up to rounding. + # The brackets' pieces sum to at most the taxable amount, up to rounding. assert np.all(amount <= incomes + 1e-6) # The amount is the tax at rates of 1 below the limit and 0 otherwise. indicators = [1.0 if rate < limit else 0.0 for rate in rates] From 80f291bd79857becf58dd74f50a13123d3ebaae2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 21:06:59 -0400 Subject: [PATCH 6/7] Cut the reform simulation tests' memory in the contrib YAML batch The contrib YAML batch for this folder, which also collects the Python tests, peaked at 10,719 MB on CI at a42a67842e, against 6,578 MB for the same folder on a recent run from main (run 37388221916). The tests built a module-level reformed system and cloned it for every simulation. Instead, apply the reform to a clone of the already loaded default system for each simulation, and collect each simulation before building the next. Locally (macOS, Python 3.14t) the folder's peak resident set fell from 14.5 GB to 9.45 GB, with the same 18 tests passing. Co-Authored-By: Claude Opus 5.5 --- ...st_additional_tax_bracket_unset_bracket.py | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py b/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py index 116eac2532b..0c886eb6ddf 100644 --- a/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py +++ b/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py @@ -59,13 +59,15 @@ any other, and not end to end. """ +import gc + import numpy as np import pytest from policyengine_core.parameters import ParameterNode from policyengine_core.reforms import Reform from policyengine_core.taxscales import MarginalRateTaxScale -from policyengine_us import CountryTaxBenefitSystem, Simulation +from policyengine_us import Simulation from policyengine_us.reforms.additional_tax_bracket.additional_tax_bracket_reform import ( additional_tax_bracket, ) @@ -419,29 +421,34 @@ def grid_situation(year): GRID_SIZE = len(STATUSES) * len(ORDINARY_INCOMES) * len(PREFERENTIAL_INCOME) -@pytest.fixture(scope="module") -def reform_system(): - """The reform's system, built once; each simulation that changes its - parameters works on its own clone.""" - return CountryTaxBenefitSystem(reform=additional_tax_bracket) - +def calculate(variables, year, reforms=None): + """The grid's values of some variables, under the reform and any parameter + changes, or under the baseline when ``reforms`` is None. -def calculate(variables, year, tax_benefit_system, reform=None): + A reformed simulation runs on its own clone of the loaded default system. + This file also runs inside the contrib YAML batch, whose memory is + budgeted, so collect each simulation before building the next. + """ sim = Simulation( - tax_benefit_system=tax_benefit_system, - reform=reform, + tax_benefit_system=baseline_system, + reform=None if reforms is None else (additional_tax_bracket, *reforms), situation=grid_situation(year), ) assert np.array_equal( sim.calculate("filing_status", year).decode_to_str(), np.repeat(STATUSES, GRID_SIZE // len(STATUSES)), ) - return {variable: sim.calculate(variable, year) for variable in variables} + values = { + variable: np.array(sim.calculate(variable, year)) for variable in variables + } + del sim + gc.collect() + return values @pytest.mark.parametrize("year", [2025, 2026]) -def test_default_parameters_give_finite_taxes(reform_system, year): - taxes = calculate(DOWNSTREAM_VARIABLES, year, reform_system) +def test_default_parameters_give_finite_taxes(year): + taxes = calculate(DOWNSTREAM_VARIABLES, year, reforms=()) for variable, values in taxes.items(): assert len(values) == GRID_SIZE assert np.all(np.isfinite(values)), variable @@ -464,14 +471,13 @@ def current_law_first_seven_brackets(year): return Reform.from_dict(changes, country_id="us") -def test_reform_with_unset_bracket_reproduces_current_law(reform_system): +def test_reform_with_unset_bracket_reproduces_current_law(): reform = calculate( DOWNSTREAM_VARIABLES, YEAR, - reform_system, - reform=current_law_first_seven_brackets(YEAR), + reforms=(current_law_first_seven_brackets(YEAR),), ) - baseline = calculate(DOWNSTREAM_VARIABLES, YEAR, baseline_system) + baseline = calculate(DOWNSTREAM_VARIABLES, YEAR) for variable in DOWNSTREAM_VARIABLES: np.testing.assert_allclose( reform[variable], baseline[variable], atol=SIMULATION_TOLERANCE @@ -520,9 +526,9 @@ def per_status(values): SHAPES_8 = {status: shape[1] for status, shape in THRESHOLD_SHAPES.items()} -# Each example runs two simulations on clones of the reform's system, and the -# file runs in two CI steps, so the generated examples are few; the schedule -# properties above take 300 examples each. +# Each example runs two simulations, each on its own clone of the default +# system, and the file runs in two CI steps, so the generated examples are +# few; the schedule properties above take 300 examples each. @hypothesis.settings( max_examples=3, deadline=None, @@ -540,20 +546,18 @@ def per_status(values): @hypothesis.example(SHAPES_7, SHAPES_8, [0.396, 0.42]) @hypothesis.example(SHAPES_7, SHAPES_8, [0.0, 1.0]) def test_reform_tax_is_finite_and_at_least_tax_without_the_bracket( - reform_system, thresholds_7, thresholds_8, rates + thresholds_7, thresholds_8, rates ): rate_7, rate_8 = rates with_bracket = calculate( REFORM_VARIABLES, YEAR, - reform_system, - reform=added_bracket(thresholds_7, thresholds_8, rate_7, rate_8), + reforms=(added_bracket(thresholds_7, thresholds_8, rate_7, rate_8),), ) removed = calculate( REFORM_VARIABLES, YEAR, - reform_system, - reform=added_bracket(thresholds_7, thresholds_8, rate_7, rate_7), + reforms=(added_bracket(thresholds_7, thresholds_8, rate_7, rate_7),), ) for variable in REFORM_VARIABLES: assert np.all(np.isfinite(with_bracket[variable])), variable From 1e302805cdb1db73a50b8df71d4beb7db1e7626b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 21:46:35 -0400 Subject: [PATCH 7/7] Check that reformed simulations leave the default system unchanged Each reformed simulation in these tests applies the reform and its parameter changes to a clone of the loaded default system. After every simulation, assert that the default system still holds its own income_tax_main_rates, regular_tax_before_credits and taxable_income_taxed_below_25_percent, and its 2026 values of the reform's schedule, so a leak into the shared system fails here rather than in a later test. The folder passes locally: 18 tests. Correction to 80f291bd79's message: run 37388221916 is a pull-request run on a main-based branch (nc-reduced-price-school-meals) whose folder holds only the YAML file, not a run on main; and the old tests cloned their module-level reformed system only for the simulations with parameter changes. Co-Authored-By: Claude Opus 5.5 --- ...st_additional_tax_bracket_unset_bracket.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py b/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py index 0c886eb6ddf..43b7ad96afb 100644 --- a/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py +++ b/policyengine_us/tests/policy/contrib/additional_tax_bracket/test_additional_tax_bracket_unset_bracket.py @@ -421,6 +421,24 @@ def grid_situation(year): GRID_SIZE = len(STATUSES) * len(ORDINARY_INCOMES) * len(PREFERENTIAL_INCOME) +def default_system_state(): + """The loaded default system's variables that the reform replaces, and its + values of the reform's schedule, which no simulation here may change.""" + bracket = baseline_system.parameters( + f"{YEAR}-01-01" + ).gov.contrib.additional_tax_bracket.bracket + variables = [baseline_system.variables[name] for name in REFORM_VARIABLES] + values = [float(bracket.rates[str(i)]) for i in range(1, N_BRACKETS + 1)] + [ + float(bracket.thresholds[str(i)][status]) + for i in range(1, N_BRACKETS + 1) + for status in STATUSES + ] + return variables, values + + +DEFAULT_SYSTEM_STATE = default_system_state() + + def calculate(variables, year, reforms=None): """The grid's values of some variables, under the reform and any parameter changes, or under the baseline when ``reforms`` is None. @@ -443,6 +461,12 @@ def calculate(variables, year, reforms=None): } del sim gc.collect() + # The reform and its parameter changes apply to the clone only. + variables_now, values_now = default_system_state() + assert all( + now is before for now, before in zip(variables_now, DEFAULT_SYSTEM_STATE[0]) + ) + assert values_now == DEFAULT_SYSTEM_STATE[1] return values