diff --git a/changelog.d/fix-ar-low-income-duplicate-threshold.fixed.md b/changelog.d/fix-ar-low-income-duplicate-threshold.fixed.md new file mode 100644 index 00000000000..afcbde8f66a --- /dev/null +++ b/changelog.d/fix-ar-low-income-duplicate-threshold.fixed.md @@ -0,0 +1 @@ +Corrected the 2024 Arkansas low-income tax table for head of household and surviving spouse filers with two or more dependents, whose $92 row shared the $24,200 threshold of the $104 row and was summed into a $196 row; removed a duplicated top bracket from the IRS SOI AGI-by-size calibration scale; and added tests that fail when any parameter scale repeats a threshold (other than +inf) or an Arkansas low-income table stops rising with AGI. diff --git a/policyengine_us/parameters/calibration/gov/irs/soi/agi/total_agi.yaml b/policyengine_us/parameters/calibration/gov/irs/soi/agi/total_agi.yaml index 229e85151d6..a5753724b4d 100644 --- a/policyengine_us/parameters/calibration/gov/irs/soi/agi/total_agi.yaml +++ b/policyengine_us/parameters/calibration/gov/irs/soi/agi/total_agi.yaml @@ -132,10 +132,3 @@ brackets: uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income values: 2015-01-01: 824_093_126_000 - - threshold: - 2015-01-01: 10_000_000 - amount: - metadata: - uprating: calibration.gov.cbo.income_by_source.adjusted_gross_income - values: - 2015-01-01: 824_093_126_000 diff --git a/policyengine_us/parameters/gov/states/ar/tax/income/rates/low_income_tax_tables/head_of_household/two_or_more_dependents.yaml b/policyengine_us/parameters/gov/states/ar/tax/income/rates/low_income_tax_tables/head_of_household/two_or_more_dependents.yaml index 7b91ee064c6..d6c88697143 100644 --- a/policyengine_us/parameters/gov/states/ar/tax/income/rates/low_income_tax_tables/head_of_household/two_or_more_dependents.yaml +++ b/policyengine_us/parameters/gov/states/ar/tax/income/rates/low_income_tax_tables/head_of_household/two_or_more_dependents.yaml @@ -26,9 +26,9 @@ metadata: - title: Arkansas Income Tax Instructions, 2021, Low Income Tax Tables href: https://www.dfa.arkansas.gov/wp-content/uploads/2021_AR1000F_and_AR1000NR_Instructions.pdf#page=26 - title: Arkansas Income Tax Instructions, 2023, Low Income Tax Tables - href: https://www.dfa.arkansas.gov/wp-content/uploads/2023_AR1000F_and_AR1000NR_Instructions.pdf#page=25 + href: https://www.dfa.arkansas.gov/wp-content/uploads/2023_AR1000F_and_AR1000NR_Instructions.pdf#page=29 - title: Arkansas Income Tax Instructions, 2024, Low Income Tax Tables - href: https://www.dfa.arkansas.gov/wp-content/uploads/2024_AR1000F_and_AR1000NR_Instructions.pdf#page=24 + href: https://www.dfa.arkansas.gov/wp-content/uploads/2024_AR1000F_and_AR1000NR_Instructions.pdf#page=28 - title: Arkansas Income Tax Instructions, 2025, Low Income Tax Tables href: https://www.dfa.arkansas.gov/wp-content/uploads/TaxTables_FI_2025.pdf#page=2 brackets: @@ -58,7 +58,7 @@ brackets: 2021-01-01: 22_125 2022-01-01: 22_789 2023-01-01: 23_472 - 2024-01-01: 24_200 + 2024-01-01: 24_176 2025-01-01: 24_818 amount: 2015-01-01: 99 diff --git a/policyengine_us/parameters/gov/states/ar/tax/income/rates/low_income_tax_tables/surviving_spouse/two_or_more_dependents.yaml b/policyengine_us/parameters/gov/states/ar/tax/income/rates/low_income_tax_tables/surviving_spouse/two_or_more_dependents.yaml index 7ed905ceb97..ef78740a31e 100644 --- a/policyengine_us/parameters/gov/states/ar/tax/income/rates/low_income_tax_tables/surviving_spouse/two_or_more_dependents.yaml +++ b/policyengine_us/parameters/gov/states/ar/tax/income/rates/low_income_tax_tables/surviving_spouse/two_or_more_dependents.yaml @@ -26,9 +26,9 @@ metadata: - title: Arkansas Income Tax Instructions, 2021, Low Income Tax Tables href: https://www.dfa.arkansas.gov/wp-content/uploads/2021_AR1000F_and_AR1000NR_Instructions.pdf#page=26 - title: Arkansas Income Tax Instructions, 2023, Low Income Tax Tables - href: https://www.dfa.arkansas.gov/wp-content/uploads/2023_AR1000F_and_AR1000NR_Instructions.pdf#page=25 + href: https://www.dfa.arkansas.gov/wp-content/uploads/2023_AR1000F_and_AR1000NR_Instructions.pdf#page=29 - title: Arkansas Income Tax Instructions, 2024, Low Income Tax Tables - href: https://www.dfa.arkansas.gov/wp-content/uploads/2024_AR1000F_and_AR1000NR_Instructions.pdf#page=24 + href: https://www.dfa.arkansas.gov/wp-content/uploads/2024_AR1000F_and_AR1000NR_Instructions.pdf#page=28 - title: Arkansas Income Tax Instructions, 2025, Low Income Tax Tables href: https://www.dfa.arkansas.gov/wp-content/uploads/TaxTables_FI_2025.pdf#page=2 brackets: @@ -58,7 +58,7 @@ brackets: 2021-01-01: 22_125 2022-01-01: 22_789 2023-01-01: 23_472 - 2024-01-01: 24_200 + 2024-01-01: 24_176 2025-01-01: 24_818 amount: 2015-01-01: 99 diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/low_income/ar_low_income_tax_joint.yaml b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/low_income/ar_low_income_tax_joint.yaml index 4f2f15109c6..49e11429e8b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/low_income/ar_low_income_tax_joint.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/low_income/ar_low_income_tax_joint.yaml @@ -334,3 +334,252 @@ state_code: AR output: ar_uses_low_income_tax_tables: false + +# The 2024 Head of Household/Surviving Spouse with 2 or More Dependents table +# (2024 AR1000F/AR1000NR instructions, PDF page 28) reads: $0-$24,176 -> $0, +# $24,177-$24,200 -> $92, $24,201-$24,300 -> $104, $24,301-$24,400 -> $116, +# $24,401-$24,500 -> $129. The $92 row's threshold was once keyed at $24,200, +# equal to the next row's, and policyengine-core sums the amounts of brackets +# with equal thresholds, so $24,201-$24,300 got $196 and the $92 row vanished. +# One single-member tax unit per income covers every row edge in the range. +- name: Case 19, 2024 head of household with 2 dependents from $24,100 to $24,401. + period: 2024 + input: + people: + person1: + ar_agi_joint: 24_100 + person2: + ar_agi_joint: 24_176 + person3: + ar_agi_joint: 24_177 + person4: + ar_agi_joint: 24_200 + person5: + ar_agi_joint: 24_201 + person6: + ar_agi_joint: 24_250 + person7: + ar_agi_joint: 24_300 + person8: + ar_agi_joint: 24_301 + person9: + ar_agi_joint: 24_400 + person10: + ar_agi_joint: 24_401 + tax_units: + tax_unit1: + members: [person1] + filing_status: HEAD_OF_HOUSEHOLD + tax_unit_dependents: 2 + tax_unit2: + members: [person2] + filing_status: HEAD_OF_HOUSEHOLD + tax_unit_dependents: 2 + tax_unit3: + members: [person3] + filing_status: HEAD_OF_HOUSEHOLD + tax_unit_dependents: 2 + tax_unit4: + members: [person4] + filing_status: HEAD_OF_HOUSEHOLD + tax_unit_dependents: 2 + tax_unit5: + members: [person5] + filing_status: HEAD_OF_HOUSEHOLD + tax_unit_dependents: 2 + tax_unit6: + members: [person6] + filing_status: HEAD_OF_HOUSEHOLD + tax_unit_dependents: 2 + tax_unit7: + members: [person7] + filing_status: HEAD_OF_HOUSEHOLD + tax_unit_dependents: 2 + tax_unit8: + members: [person8] + filing_status: HEAD_OF_HOUSEHOLD + tax_unit_dependents: 2 + tax_unit9: + members: [person9] + filing_status: HEAD_OF_HOUSEHOLD + tax_unit_dependents: 2 + tax_unit10: + members: [person10] + filing_status: HEAD_OF_HOUSEHOLD + tax_unit_dependents: 2 + households: + household: + members: [person1, person2, person3, person4, person5, person6, person7, person8, person9, person10] + state_code: AR + output: + ar_low_income_tax_joint: [0, 0, 92, 92, 104, 104, 104, 116, 116, 129] + +- name: Case 20, 2024 surviving spouse with 2 dependents from $24,100 to $24,401. + period: 2024 + input: + people: + person1: + ar_agi_joint: 24_100 + person2: + ar_agi_joint: 24_176 + person3: + ar_agi_joint: 24_177 + person4: + ar_agi_joint: 24_200 + person5: + ar_agi_joint: 24_201 + person6: + ar_agi_joint: 24_250 + person7: + ar_agi_joint: 24_300 + person8: + ar_agi_joint: 24_301 + person9: + ar_agi_joint: 24_400 + person10: + ar_agi_joint: 24_401 + tax_units: + tax_unit1: + members: [person1] + filing_status: SURVIVING_SPOUSE + tax_unit_dependents: 2 + tax_unit2: + members: [person2] + filing_status: SURVIVING_SPOUSE + tax_unit_dependents: 2 + tax_unit3: + members: [person3] + filing_status: SURVIVING_SPOUSE + tax_unit_dependents: 2 + tax_unit4: + members: [person4] + filing_status: SURVIVING_SPOUSE + tax_unit_dependents: 2 + tax_unit5: + members: [person5] + filing_status: SURVIVING_SPOUSE + tax_unit_dependents: 2 + tax_unit6: + members: [person6] + filing_status: SURVIVING_SPOUSE + tax_unit_dependents: 2 + tax_unit7: + members: [person7] + filing_status: SURVIVING_SPOUSE + tax_unit_dependents: 2 + tax_unit8: + members: [person8] + filing_status: SURVIVING_SPOUSE + tax_unit_dependents: 2 + tax_unit9: + members: [person9] + filing_status: SURVIVING_SPOUSE + tax_unit_dependents: 2 + tax_unit10: + members: [person10] + filing_status: SURVIVING_SPOUSE + tax_unit_dependents: 2 + households: + household: + members: [person1, person2, person3, person4, person5, person6, person7, person8, person9, person10] + state_code: AR + output: + ar_low_income_tax_joint: [0, 0, 92, 92, 104, 104, 104, 116, 116, 129] + +- name: Case 21, 2024 head of household and surviving spouse with 2 dependents in the last table rows. + period: 2024 + input: + people: + person1: + ar_agi_joint: 28_100 + person2: + ar_agi_joint: 28_101 + person3: + ar_agi_joint: 28_200 + person4: + ar_agi_joint: 28_100 + person5: + ar_agi_joint: 28_101 + person6: + ar_agi_joint: 28_200 + tax_units: + tax_unit1: + members: [person1] + filing_status: HEAD_OF_HOUSEHOLD + tax_unit_dependents: 2 + tax_unit2: + members: [person2] + filing_status: HEAD_OF_HOUSEHOLD + tax_unit_dependents: 2 + tax_unit3: + members: [person3] + filing_status: HEAD_OF_HOUSEHOLD + tax_unit_dependents: 2 + tax_unit4: + members: [person4] + filing_status: SURVIVING_SPOUSE + tax_unit_dependents: 2 + tax_unit5: + members: [person5] + filing_status: SURVIVING_SPOUSE + tax_unit_dependents: 2 + tax_unit6: + members: [person6] + filing_status: SURVIVING_SPOUSE + tax_unit_dependents: 2 + households: + household: + members: [person1, person2, person3, person4, person5, person6] + state_code: AR + output: + # $28,001-$28,100 -> $575; $28,101-$28,200 -> $588. + ar_low_income_tax_joint: [575, 588, 588, 575, 588, 588] + +- name: Case 22, 2024 head of household and surviving spouse with 2 dependents above the $28,200 cutoff. + period: 2024 + input: + people: + person1: + ar_agi_joint: 28_201 + person2: + ar_agi_joint: 28_201 + tax_units: + tax_unit1: + members: [person1] + filing_status: HEAD_OF_HOUSEHOLD + tax_unit_dependents: 2 + tax_unit2: + members: [person2] + filing_status: SURVIVING_SPOUSE + tax_unit_dependents: 2 + households: + household: + members: [person1, person2] + state_code: AR + output: + ar_uses_low_income_tax_tables: [false, false] + +- name: Case 23, 2024 head of household with 2 dependents pays the table tax before credits. + period: 2024 + input: + people: + person1: + ar_agi_joint: 24_190 + person2: + ar_agi_joint: 24_250 + tax_units: + tax_unit1: + members: [person1] + filing_status: HEAD_OF_HOUSEHOLD + tax_unit_dependents: 2 + tax_unit2: + members: [person2] + filing_status: HEAD_OF_HOUSEHOLD + tax_unit_dependents: 2 + households: + household: + members: [person1, person2] + state_code: AR + output: + ar_low_income_tax_joint: [92, 104] + ar_income_tax_before_non_refundable_credits_joint: [92, 104] diff --git a/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/low_income/test_ar_low_income_tax_tables.py b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/low_income/test_ar_low_income_tax_tables.py new file mode 100644 index 00000000000..0b06490d828 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ar/tax/income/low_income/test_ar_low_income_tax_tables.py @@ -0,0 +1,64 @@ +"""Invariants of the Arkansas low-income tax tables, checked at every integer AGI. + +`ar_low_income_tax_joint` evaluates these scales with `right=True`. The +published tables charge more tax at higher AGI until a cutoff, above which +filers use the regular tax table (encoded as an infinite amount). The head of +household and surviving spouse files encode the same published column +("Filing Status 3 or 6"), so they must agree. A 2024 data error broke the +first invariant: two brackets shared a threshold, core summed them, and the +table charged $196 at $24,201-$24,300 and then $116 at $24,301-$24,400. +""" + +import numpy as np +import pytest +from policyengine_core.parameters import ParameterScale + +from policyengine_us.system import system + +TABLES = system.parameters.gov.states.ar.tax.income.rates.low_income_tax_tables +SCALES = { + node.name.removeprefix(TABLES.name + "."): node + for node in TABLES.get_descendants() + if isinstance(node, ParameterScale) +} +YEARS = range(2015, 2031) + + +def _tax_by_integer_agi(scale, year): + at_instant = scale(f"{year}-01-01") + top = max(t for t in at_instant.thresholds if np.isfinite(t)) + agi = np.arange(0, int(top) + 2) + return at_instant.calc(agi, right=True) + + +def test_tables_cover_every_filing_status_and_dependent_tier(): + assert sorted(SCALES) == [ + "head_of_household.no_or_one_dependent", + "head_of_household.two_or_more_dependents", + "joint.no_or_one_dependent", + "joint.two_or_more_dependents", + "single", + "surviving_spouse.no_or_one_dependent", + "surviving_spouse.two_or_more_dependents", + ] + + +@pytest.mark.parametrize("year", YEARS) +@pytest.mark.parametrize("table", sorted(SCALES)) +def test_tax_rises_with_agi_until_the_cutoff(table, year): + tax = _tax_by_integer_agi(SCALES[table], year) + finite = np.isfinite(tax) + + assert finite[0] and not finite[-1] + # Once AGI passes the cutoff it never re-enters the table. + assert not np.any(finite[1:] & ~finite[:-1]) + assert np.all(np.diff(tax[finite]) >= 0) + + +@pytest.mark.parametrize("year", YEARS) +@pytest.mark.parametrize("tier", ["no_or_one_dependent", "two_or_more_dependents"]) +def test_head_of_household_and_surviving_spouse_tables_agree(tier, year): + head_of_household = _tax_by_integer_agi(SCALES[f"head_of_household.{tier}"], year) + surviving_spouse = _tax_by_integer_agi(SCALES[f"surviving_spouse.{tier}"], year) + + np.testing.assert_array_equal(head_of_household, surviving_spouse) diff --git a/policyengine_us/tests/test_parameter_scale_thresholds.py b/policyengine_us/tests/test_parameter_scale_thresholds.py new file mode 100644 index 00000000000..0899577cbed --- /dev/null +++ b/policyengine_us/tests/test_parameter_scale_thresholds.py @@ -0,0 +1,262 @@ +"""Parameter scales must not repeat a threshold at any instant. + +To build a scale at an instant, policyengine-core's +`ParameterScale._get_at_instant` passes brackets to `add_bracket`, which adds +a bracket's amount (or rate) to an existing row with an equal threshold +instead of keeping a second row. Two brackets keyed to the same threshold +therefore collapse into one row that carries their sum, with no error. The +2024 Arkansas low-income tax tables for head of household and surviving +spouse filers with two or more dependents shipped this way: the $92 row was +keyed at $24,200, the $104 row's threshold, so AGI of $24,201-$24,300 got +$196 and the $92 row disappeared. + +Duplicate `+inf` thresholds are exempt: tables whose length changes over +time park unused brackets there, and no finite value reaches them. `-inf` is +a reachable bottom row, so duplicates there are flagged. +""" + +import math +from collections import defaultdict +from datetime import date + +import pytest +from policyengine_core.parameters import ParameterNode, ParameterScale +from policyengine_core.parameters.operations.uprate_parameters import ( + uprate_parameters, +) +from policyengine_core.taxscales.amount_tax_scale_like import AmountTaxScaleLike +from policyengine_core.taxscales.rate_tax_scale_like import RateTaxScaleLike + +from policyengine_us.system import system + + +def _add_bracket_thresholds(scale, instant_str): + """(bracket index, threshold) for each bracket that + `ParameterScale._get_at_instant` passes to `add_bracket` at the instant. + + Mirrors core: a bracket joins only when its threshold and its value child + (chosen by scale type, as core chooses it) are both non-None at the + instant. Values come from `Parameter._get_at_instant`, which takes the + first `values_list` entry on or before the instant; calling it with the + string directly also handles padded 0000-01-01 entries, which the public + lookup cannot turn into a date. + """ + present = [ + { + name: value + for name, child in bracket.children.items() + if (value := child._get_at_instant(instant_str)) is not None + } + for bracket in scale.brackets + ] + if scale.metadata.get("type") == "single_amount" or any( + "amount" in children for children in present + ): + value_key = "amount" + elif any("average_rate" in children for children in present): + value_key = "average_rate" + else: + value_key = "rate" + return [ + (index, children["threshold"]) + for index, children in enumerate(present) + if "threshold" in children and value_key in children + ] + + +def _change_instants(scale): + # A collision can start when a threshold changes or when a bracket's + # amount or rate starts, so check every date any bracket child changes. + return sorted( + { + value_at_instant.instant_str + for bracket in scale.brackets + for child in bracket.children.values() + for value_at_instant in getattr(child, "values_list", []) + } + ) + + +def _duplicate_threshold_errors(scale): + instants_by_collision = defaultdict(list) + for instant_str in _change_instants(scale): + brackets_by_threshold = defaultdict(list) + for index, threshold in _add_bracket_thresholds(scale, instant_str): + if threshold != math.inf: + brackets_by_threshold[float(threshold)].append(index) + for threshold, indices in brackets_by_threshold.items(): + if len(indices) > 1: + instants_by_collision[(tuple(indices), threshold)].append(instant_str) + errors = [] + for (indices, threshold), instants in instants_by_collision.items(): + when = instants[0] + if len(instants) > 1: + when = f"{len(instants)} instants from {instants[0]} to {instants[-1]}" + errors.append( + f"{scale.name}: brackets {list(indices)} share threshold " + f"{threshold:,.15g} at {when}" + ) + return errors + + +def _scale(brackets): + return ParameterScale( + "test_scale", + {"metadata": {"type": "single_amount"}, "brackets": brackets}, + "test_scale.yaml", + ) + + +def _all_scales(): + return [ + node + for node in system.parameters.get_descendants() + if isinstance(node, ParameterScale) + ] + + +def test_duplicate_threshold_guard_flags_a_later_collision(): + scale = _scale( + [ + {"threshold": {"2021-01-01": 0}, "amount": {"2021-01-01": 0}}, + { + "threshold": {"2021-01-01": 100, "2024-01-01": 200}, + "amount": {"2021-01-01": 1}, + }, + {"threshold": {"2021-01-01": 200}, "amount": {"2021-01-01": 2}}, + ] + ) + + assert _duplicate_threshold_errors(scale) == [ + "test_scale: brackets [1, 2] share threshold 200 at 2024-01-01" + ] + + +def test_duplicate_threshold_guard_ignores_only_positive_infinity(): + def scale_with_two_brackets_at(threshold): + return _scale( + [ + {"threshold": {"2021-01-01": threshold}, "amount": {"2021-01-01": 1}}, + {"threshold": {"2021-01-01": threshold}, "amount": {"2021-01-01": 2}}, + ] + ) + + assert _duplicate_threshold_errors(scale_with_two_brackets_at(math.inf)) == [] + assert _duplicate_threshold_errors(scale_with_two_brackets_at(-math.inf)) == [ + "test_scale: brackets [0, 1] share threshold -inf at 2021-01-01" + ] + + +def test_duplicate_threshold_guard_skips_brackets_core_skips(): + # Core leaves out a bracket whose amount has not started yet, so the + # shared threshold only collides once bracket 1's amount begins. + scale = _scale( + [ + {"threshold": {"2021-01-01": 100}, "amount": {"2021-01-01": 1}}, + {"threshold": {"2021-01-01": 100}, "amount": {"2024-01-01": 2}}, + ] + ) + + assert _duplicate_threshold_errors(scale) == [ + "test_scale: brackets [0, 1] share threshold 100 at 2024-01-01" + ] + + +def _uprated_scale(other_threshold): + # Uprating from start_instant writes a second 2022-01-01 entry after the + # explicit one; core's lookup takes the explicit 300. + root = ParameterNode( + "root", + data={ + "index": {"values": {"2021-01-01": 1, "2022-01-01": 2}}, + "table": { + "metadata": {"type": "single_amount"}, + "brackets": [ + { + "threshold": { + "values": {"2021-01-01": 100, "2022-01-01": 300}, + "metadata": { + "uprating": { + "parameter": "index", + "start_instant": "2021-01-01", + } + }, + }, + "amount": {"2021-01-01": 10}, + }, + { + "threshold": {"2021-01-01": other_threshold}, + "amount": {"2021-01-01": 20}, + }, + ], + }, + }, + ) + uprate_parameters(root) + return root.table + + +def test_duplicate_threshold_guard_reads_tied_dates_like_core(): + colliding = _uprated_scale(300) + distinct = _uprated_scale(200) + tied = [ + entry.instant_str + for entry in colliding.brackets[0].children["threshold"].values_list + ] + + if tied.count("2022-01-01") != 2: + pytest.skip("core's uprating no longer writes a tied 2022-01-01 entry") + assert _duplicate_threshold_errors(colliding) == [ + "root.table: brackets [0, 1] share threshold 300 at 2022-01-01" + ] + assert _duplicate_threshold_errors(distinct) == [] + + +def _is_calendar_date(instant_str): + # Core's public lookup parses the instant, which fails for padded + # 0000-01-01 entries and for 2021-06-31 in + # gov.states.ny.nyserda.drive_clean.amount. + try: + date.fromisoformat(instant_str) + except ValueError: + return False + return True + + +def test_add_bracket_thresholds_match_core_for_every_scale_and_instant(monkeypatch): + # Record every threshold core passes to add_bracket, repeats included, and + # compare that multiset with the guard's; comparing the built scale's + # thresholds would hide exactly the repeats the guard exists to find. + passed = [] + for scale_class in (AmountTaxScaleLike, RateTaxScaleLike): + original = scale_class.add_bracket + + def recording_add_bracket(self, threshold, value, original=original): + passed.append(float(threshold)) + return original(self, threshold, value) + + monkeypatch.setattr(scale_class, "add_bracket", recording_add_bracket) + + mismatches = [] + for scale in _all_scales(): + for instant_str in _change_instants(scale): + if not _is_calendar_date(instant_str): + continue + passed.clear() + scale(instant_str) + expected = sorted( + float(threshold) + for _, threshold in _add_bracket_thresholds(scale, instant_str) + ) + if expected != sorted(passed): + mismatches.append(f"{scale.name} at {instant_str}") + + assert not mismatches, "\n".join(mismatches) + + +def test_parameter_scales_have_no_duplicate_thresholds(): + scales = _all_scales() + errors = [error for scale in scales for error in _duplicate_threshold_errors(scale)] + + assert scales + assert not errors, "\n".join(errors)