From 78fb890c98af8232269189805b345e73113a1b60 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 11:13:29 -0400 Subject: [PATCH 1/2] Count Ohio's unreimbursed medical care deduction once per return oh_unreimbursed_medical_care_expense_deduction_person listed the TaxUnit variable oh_insured_unreimbursed_medical_care_expenses in its adds. policyengine-core evaluates adds with map_to="person", which projects a group value onto every member, so each member deducted the full worksheet line 8 amount and Ohio AGI (the sum over members) fell by n times it. Each spouse's joint filing credit qualifying income also lost the whole amount. Use the existing person-level oh_insured_unreimbursed_medical_care_expenses_person, which prorates line 8 by each member's share of line 5 and sums to the return amount. Also enter federal AGI below zero as zero on worksheet line 6, as every year's instructions say, so line 8 never exceeds line 5. Adds YAML tests that fail on main (person split, total once, Ohio AGI, joint filing credit, negative AGI) and Hypothesis invariants (conservation, an independent worksheet, bounds, once-only accounting). Updates the 2026 retired-couple case, whose Medicare Part B line 8 had been deducted twice. Co-Authored-By: Claude Opus 5.5 --- ...x-oh-medical-deduction-per-person.fixed.md | 1 + ...reimbursed_medical_deduction_invariants.py | 278 ++++++++++++++++++ ...ed_unreimbursed_medical_care_expenses.yaml | 20 ++ ...bursed_medical_care_expense_deduction.yaml | 145 +++++++++ .../oh/tax/income/integration_2025.yaml | 12 +- ...ured_unreimbursed_medical_care_expenses.py | 10 +- ...d_medical_care_expense_deduction_person.py | 11 +- 7 files changed, 465 insertions(+), 12 deletions(-) create mode 100644 changelog.d/fix-oh-medical-deduction-per-person.fixed.md create mode 100644 policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py create mode 100644 policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_unreimbursed_medical_care_expense_deduction.yaml diff --git a/changelog.d/fix-oh-medical-deduction-per-person.fixed.md b/changelog.d/fix-oh-medical-deduction-per-person.fixed.md new file mode 100644 index 00000000000..fba13acbb74 --- /dev/null +++ b/changelog.d/fix-oh-medical-deduction-per-person.fixed.md @@ -0,0 +1 @@ +Count Ohio's unreimbursed medical care deduction once per return: each member now takes their prorated share of worksheet line 8 rather than the full return amount, and federal AGI below zero is entered as zero on worksheet line 6. diff --git a/policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py b/policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py new file mode 100644 index 00000000000..2f4e8067ebb --- /dev/null +++ b/policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py @@ -0,0 +1,278 @@ +"""Invariants for Ohio's unreimbursed medical care deduction, R.C. 5747.01(A)(10). + +The Unreimbursed Medical Care Expenses Worksheet (2025 IT 1040 instructions, +p. 41) computes line 8 once for the return: line 5 expenses less 7.5% of +federal AGI, with AGI entered as zero if less than zero (line 6). Lines 1 and 2 +(premiums with no Medicare or employer plan, and long-term care premiums) are +deducted in full. Each person carries their own lines 1 and 2 and a share of +line 8 in proportion to their line 5 expenses, and Ohio AGI is the sum over +the members. + +Hypothesis draws batches of tax units (single, joint, head of household or +joint with dependents), each person with wages, medical expenses, uninsured +premiums and long-term care premiums, and some heads with a business loss +that makes federal AGI negative. A seeded population of 200 units adds +breadth. Each batch runs as one vectorized simulation, and again with every +medical amount set to zero. For every tax unit: + +1. Conservation: the members' line 8 shares sum to the return's line 8. +2. Differential: line 8 and each share equal an independent numpy worksheet, + with the 7.5% rate taken from the instructions rather than the parameters. +3. Bounds: 0 <= line 8 <= line 5, and 0 <= each share <= the person's own + line 5 expenses. +4. Accounting: the tax unit deduction is line 8 plus lines 1 and 2, counted + once. Removing the medical amounts raises Ohio AGI by exactly that + deduction, and raises each person's Ohio AGI (and joint filing credit + qualifying income) by exactly their own share, so a spouse never loses + qualifying income for the other spouse's expenses. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation + +TOLERANCE = 0.01 # dollars + +# Worksheet line 7 multiplies line 6 by 7.5% in every year's instructions +# (2022 p. 27, 2023 p. 40, 2024 p. 41, 2025 p. 41). +PUBLISHED_RATE = 0.075 +YEARS = [2022, 2024, 2025] + +MEDICAL = [ + "other_medical_expenses", + "health_insurance_premiums", + "long_term_health_insurance_premiums", +] + +amounts = st.one_of( + st.just(0.0), + st.integers(1, 3_000).map(float), + st.integers(3_000, 40_000).map(float), +) +wages = st.one_of(st.just(0.0), st.integers(1, 200_000).map(float)) +losses = st.one_of(st.just(0.0), st.integers(-100_000, -1).map(float)) + + +@st.composite +def people(draw): + return { + "employment_income": draw(wages), + "other_medical_expenses": draw(amounts), + "health_insurance_premiums": draw(amounts), + "long_term_health_insurance_premiums": draw(amounts), + } + + +@st.composite +def tax_units(draw): + kind = draw(st.sampled_from(["single", "joint", "hoh", "joint_dependents"])) + n_dependents = draw(st.integers(1, 3)) if kind in ("hoh", "joint_dependents") else 0 + return { + "head": draw(people()), + "loss": draw(losses), + "spouse": draw(people()) if kind.startswith("joint") else None, + "dependents": [draw(people()) for _ in range(n_dependents)], + } + + +SEED = 20261006 + + +def _seeded_units(n=200): + rng = np.random.default_rng(SEED) + + def amount(): + return float( + [0, rng.integers(1, 3_001), rng.integers(3_000, 40_001)][rng.choice(3)] + ) + + def person(): + return { + "employment_income": float(rng.integers(0, 200_001)), + "other_medical_expenses": amount(), + "health_insurance_premiums": amount(), + "long_term_health_insurance_premiums": amount(), + } + + units = [] + for _ in range(n): + kind = rng.choice(["single", "joint", "hoh", "joint_dependents"]) + n_dependents = ( + int(rng.integers(1, 4)) if kind in ("hoh", "joint_dependents") else 0 + ) + units.append( + { + "head": person(), + "loss": float(-rng.integers(1, 100_001)) if rng.random() < 0.2 else 0.0, + "spouse": person() if kind.startswith("joint") else None, + "dependents": [person() for _ in range(n_dependents)], + } + ) + return units + + +def _situation(units, year, *, medical): + people, groups = {}, {"tax_units": {}, "households": {}, "marital_units": {}} + + def add(name, values, role): + people[name] = { + # Under 65 and not disabled, so no one is eligible for Medicare + # and worksheet line 3 is zero. + "age": {"head": 45, "spouse": 43, "dependent": 10}[role], + "is_tax_unit_head": role == "head", + "is_tax_unit_spouse": role == "spouse", + "is_tax_unit_dependent": role == "dependent", + "employment_income": values["employment_income"], + "employer_contribution_to_health_insurance_premiums_category": "NONE", + **{name: values[name] * medical for name in MEDICAL}, + } + + for i, unit in enumerate(units): + head = f"head_{i}" + add(head, unit["head"], "head") + people[head]["self_employment_income"] = unit["loss"] + members, couple = [head], [head] + if unit["spouse"] is not None: + spouse = f"spouse_{i}" + add(spouse, unit["spouse"], "spouse") + members.append(spouse) + couple.append(spouse) + groups["marital_units"][f"couple_{i}"] = {"members": couple} + for j, dependent in enumerate(unit["dependents"]): + name = f"dependent_{i}_{j}" + add(name, dependent, "dependent") + members.append(name) + groups["marital_units"][f"single_{i}_{j}"] = {"members": [name]} + groups["tax_units"][f"tax_unit_{i}"] = {"members": members} + groups["households"][f"household_{i}"] = { + "members": members, + "state_code": {year: "OH"}, + } + people = { + name: {key: {year: value} for key, value in values.items()} + for name, values in people.items() + } + return {"people": people, **groups} + + +PERSON = [ + "other_medical_expenses", + "oh_insured_unreimbursed_medical_care_expense_amount", + "oh_insured_unreimbursed_medical_care_expenses_person", + "oh_uninsured_unreimbursed_medical_care_expenses", + "long_term_health_insurance_premiums", + "oh_unreimbursed_medical_care_expense_deduction_person", + "oh_agi_person", + "oh_joint_filing_credit_qualifying_income", +] +TAX_UNIT = [ + "adjusted_gross_income", + "oh_insured_unreimbursed_medical_care_expenses", + "oh_unreimbursed_medical_care_expense_deduction", + "oh_agi", +] + + +def _run(units, year, *, medical=True): + sim = Simulation(situation=_situation(units, year, medical=medical)) + out = { + name: np.asarray(sim.calculate(name, year), dtype=float) + for name in PERSON + TAX_UNIT + } + out["filer"] = np.asarray(sim.calculate("is_tax_unit_head_or_spouse", year)) + out["unit"] = sim.populations["tax_unit"].members_entity_id + return out + + +def _unit_sum(run, values): + return np.bincount(run["unit"], weights=values, minlength=len(run["oh_agi"])) + + +def _check(units, year): + base = _run(units, year) + unit = base["unit"] + line_5_person = base["oh_insured_unreimbursed_medical_care_expense_amount"] + line_8 = base["oh_insured_unreimbursed_medical_care_expenses"] + share = base["oh_insured_unreimbursed_medical_care_expenses_person"] + + # No one is Medicare eligible, so line 5 is each person's medical care. + np.testing.assert_allclose( + line_5_person, base["other_medical_expenses"], atol=TOLERANCE + ) + line_5 = _unit_sum(base, line_5_person) + + # 1. Conservation. + np.testing.assert_allclose(_unit_sum(base, share), line_8, atol=TOLERANCE) + + # 2. Differential against an independent worksheet. + line_6 = np.maximum(base["adjusted_gross_income"], 0) + expected_line_8 = np.maximum(line_5 - PUBLISHED_RATE * line_6, 0) + np.testing.assert_allclose(line_8, expected_line_8, atol=TOLERANCE) + expected_share = np.divide( + line_5_person * expected_line_8[unit], + line_5[unit], + out=np.zeros_like(line_5_person), + where=line_5[unit] > 0, + ) + np.testing.assert_allclose(share, expected_share, atol=TOLERANCE) + + # 3. Bounds. + assert (line_8 >= 0).all() + assert (line_8 <= line_5 + TOLERANCE).all() + assert (share >= 0).all() + assert (share <= line_5_person + TOLERANCE).all() + + # 4. Accounting: each amount counted once, and only for its own person. + person_deduction = base["oh_unreimbursed_medical_care_expense_deduction_person"] + np.testing.assert_allclose( + person_deduction, + share + + base["oh_uninsured_unreimbursed_medical_care_expenses"] + + base["long_term_health_insurance_premiums"], + atol=TOLERANCE, + ) + deduction = base["oh_unreimbursed_medical_care_expense_deduction"] + np.testing.assert_allclose( + deduction, _unit_sum(base, person_deduction), atol=TOLERANCE + ) + + without = _run(units, year, medical=False) + np.testing.assert_allclose( + without["adjusted_gross_income"], + base["adjusted_gross_income"], + atol=TOLERANCE, + ) + np.testing.assert_allclose( + without["oh_unreimbursed_medical_care_expense_deduction"], 0, atol=TOLERANCE + ) + np.testing.assert_allclose( + without["oh_agi"] - base["oh_agi"], deduction, atol=TOLERANCE + ) + for name in ["oh_agi_person", "oh_joint_filing_credit_qualifying_income"]: + np.testing.assert_allclose( + without[name] - base[name], person_deduction, atol=TOLERANCE, err_msg=name + ) + + +# A batch's cost is mostly per-variable overhead, so each example is a large +# batch and there are few examples. +SETTINGS = dict( + max_examples=6, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +@settings(**SETTINGS) +@given(st.sampled_from(YEARS), st.lists(tax_units(), min_size=5, max_size=30)) +def test_oh_unreimbursed_medical_deduction_invariants(year, units): + _check(units, year) + + +@pytest.mark.parametrize("year", YEARS) +def test_seeded_population(year): + _check(_seeded_units(), year) diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses.yaml index 891a60ce10c..b6f9393d6dc 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses.yaml @@ -35,3 +35,23 @@ adjusted_gross_income: 1_000 output: oh_insured_unreimbursed_medical_care_expenses: 0 + +- name: Negative federal AGI is entered as zero on line 6 + period: 2025 + input: + state_code: OH + oh_insured_unreimbursed_medical_care_expense_amount: 1_000 + adjusted_gross_income: -20_000 + output: + # Worksheet line 6 is max(-20,000, 0) = 0, so line 7 is 0 and line 8 is + # 1,000 - 0 = 1,000. The deduction never exceeds the expenses paid. + oh_insured_unreimbursed_medical_care_expenses: 1_000 + +- name: Negative federal AGI with no expenses gives no deduction + period: 2025 + input: + state_code: OH + oh_insured_unreimbursed_medical_care_expense_amount: 0 + adjusted_gross_income: -20_000 + output: + oh_insured_unreimbursed_medical_care_expenses: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_unreimbursed_medical_care_expense_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_unreimbursed_medical_care_expense_deduction.yaml new file mode 100644 index 00000000000..5691fa861dc --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/deductions/medical_expense/oh_unreimbursed_medical_care_expense_deduction.yaml @@ -0,0 +1,145 @@ +# The Unreimbursed Medical Care Expenses Worksheet (2025 instructions, p. 41) +# computes line 8 once for the return. Each person carries their share of it, +# in proportion to their line 5 expenses, so the return deducts it once. + +- name: Joint couple and child, head has all the expenses + period: 2025 + input: + people: + head: + age: 40 + employment_income: 40_000 + other_medical_expenses: 6_000 + spouse: + age: 38 + employment_income: 10_000 + child: + age: 5 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse, child] + state_code: OH + output: + # Line 5 = 6,000; line 6 = 50,000; line 7 = 50,000 * 0.075 = 3,750; + # line 8 = 6,000 - 3,750 = 2,250, all of it the head's share. + oh_insured_unreimbursed_medical_care_expenses: 2_250 + oh_insured_unreimbursed_medical_care_expenses_person: [2_250, 0, 0] + oh_unreimbursed_medical_care_expense_deduction_person: [2_250, 0, 0] + oh_unreimbursed_medical_care_expense_deduction: 2_250 + # 40,000 - 2,250; 10,000; 0. + oh_agi_person: [37_750, 10_000, 0] + # 50,000 - 2,250. + oh_agi: 47_750 + +- name: Expenses for every member are prorated by each person's line 5 share + period: 2025 + input: + people: + head: + age: 40 + employment_income: 40_000 + other_medical_expenses: 3_000 + spouse: + age: 38 + employment_income: 10_000 + other_medical_expenses: 1_000 + child: + age: 5 + is_tax_unit_dependent: true + other_medical_expenses: 2_000 + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse, child] + state_code: OH + output: + # Line 5 = 6,000; line 8 = 6,000 - 3,750 = 2,250. + oh_insured_unreimbursed_medical_care_expenses: 2_250 + # 2,250 * 3/6, 2,250 * 1/6, 2,250 * 2/6. + oh_unreimbursed_medical_care_expense_deduction_person: [1_125, 375, 750] + oh_unreimbursed_medical_care_expense_deduction: 2_250 + oh_agi_person: [38_875, 9_625, -750] + oh_agi: 47_750 + +- name: Uninsured and long-term care premiums stay with the person who paid them + period: 2025 + input: + people: + head: + age: 40 + employment_income: 40_000 + other_medical_expenses: 6_000 + health_insurance_premiums: 1_200 + employer_contribution_to_health_insurance_premiums_category: NONE + spouse: + age: 38 + employment_income: 10_000 + long_term_health_insurance_premiums: 500 + child: + age: 5 + is_tax_unit_dependent: true + tax_units: + tax_unit: + members: [head, spouse, child] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse, child] + state_code: OH + output: + # Line 1 = 1,200 (head, no Medicare or employer plan); line 2 = 500 + # (spouse); line 8 = 6,000 - 3,750 = 2,250 (head). + oh_uninsured_unreimbursed_medical_care_expenses: [1_200, 0, 0] + oh_unreimbursed_medical_care_expense_deduction_person: [3_450, 500, 0] + # Line 9 = 1,200 + 500 + 2,250. + oh_unreimbursed_medical_care_expense_deduction: 3_950 + oh_agi: 46_050 + +- name: The head's medical deduction does not reduce the spouse's joint filing credit income + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 60_000 + other_medical_expenses: 10_000 + spouse: + age: 38 + employment_income: 2_000 + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: OH + output: + # Line 5 = 10,000; line 7 = 62,000 * 0.075 = 4,650; line 8 = 5,350. + oh_unreimbursed_medical_care_expense_deduction_person: [5_350, 0] + oh_unreimbursed_medical_care_expense_deduction: 5_350 + oh_agi: 56_650 + # The spouse keeps 2,000 of qualifying income, above the 500 threshold. + oh_joint_filing_credit_qualifying_income: [54_650, 2_000] + oh_joint_filing_credit_eligible: true + # Exemptions 2 * 2,150 = 4,300 (p. 17, MAGI 40,001-80,000), taxable 52,350, + # tax 342 + 2.75% * (52,350 - 26,050) = 1,065.25 (2025 instructions, p. 18). + # MAGI less exemptions is 52,350, so the credit is 10% = 106.53. + oh_joint_filing_credit: 106.53 diff --git a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration_2025.yaml b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration_2025.yaml index 3f85a33e02d..7490f01777e 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration_2025.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/oh/tax/income/integration_2025.yaml @@ -266,9 +266,15 @@ members: [person1, person2] state_code: OH output: - oh_taxable_income: 35_038.3 - oh_income_tax_before_non_refundable_credits: 579.18 + # Unreimbursed medical care worksheet: line 3 is each spouse's Medicare + # Part B premium, 2 * 2,434.80 = 4,869.60; federal AGI is 40,000 + + # 23,850 taxable Social Security = 63,850, so line 7 is 4,788.75 and + # line 8 is 80.85, deducted once for the return. + # Ohio AGI = 63,850 - 23,850 - 80.85 = 39,919.15; less 2 * 2,400 exemptions. + oh_taxable_income: 35_119.15 + # 332.00 + 2.75% * (35,119.15 - 26,050). + oh_income_tax_before_non_refundable_credits: 581.40 oh_retirement_credit_potential: 200 oh_retirement_credit: 200 oh_non_refundable_credits: 250 - state_income_tax: 329.18 + state_income_tax: 331.40 diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses.py index 56f9870f755..3a9acf8f65b 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/insured_unreimbursed_expenses/oh_insured_unreimbursed_medical_care_expenses.py @@ -8,9 +8,9 @@ class oh_insured_unreimbursed_medical_care_expenses(Variable): unit = USD definition_period = YEAR reference = ( - "https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=18", # Line 36 - "https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=27", - "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(10) + "https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=18", # Line 36 + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=41", # Worksheet + "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(A)(10)(b) ) defined_for = StateCode.OH @@ -21,8 +21,8 @@ def formula(tax_unit, period, parameters): period, ["oh_insured_unreimbursed_medical_care_expense_amount"], ) - # Line 6 - federal_agi = tax_unit("adjusted_gross_income", period) + # Line 6: federal AGI, entered as zero if less than zero. + federal_agi = max_(tax_unit("adjusted_gross_income", period), 0) # Can deduct medical expenses in excess of 7.5% of federal AGI. # Line 7 diff --git a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction_person.py b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction_person.py index d20a345b203..5dc4426d205 100644 --- a/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction_person.py +++ b/policyengine_us/variables/gov/states/oh/tax/income/deductions/medical_exepenses/oh_unreimbursed_medical_care_expense_deduction_person.py @@ -8,14 +8,17 @@ class oh_unreimbursed_medical_care_expense_deduction_person(Variable): unit = USD definition_period = YEAR reference = ( - "https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=18", # Line 36 - "https://tax.ohio.gov/static/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=27", - "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(10) + "https://dam.assets.ohio.gov/image/upload/tax.ohio.gov/forms/ohio_individual/individual/2022/it1040-sd100-instruction-booklet.pdf#page=18", # Line 36 + "https://dam.assets.ohio.gov/image/upload/v1767095693/tax.ohio.gov/forms/ohio_individual/individual/2025/it1040-booklet.pdf#page=41", # Worksheet + "https://codes.ohio.gov/ohio-revised-code/section-5747.01", # R.C. 5747.01(A)(10) ) defined_for = StateCode.OH + # The worksheet computes line 8 once for the return. Each person takes + # their share of it, so the shares sum to the return's amount. Listing + # the tax unit amount here would give every member the full amount. adds = [ - "oh_insured_unreimbursed_medical_care_expenses", + "oh_insured_unreimbursed_medical_care_expenses_person", "oh_uninsured_unreimbursed_medical_care_expenses", "long_term_health_insurance_premiums", ] From 949dd8d728c89b6e5ef12a0c8f9b69bfa751f9b4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 14:19:53 -0400 Subject: [PATCH 2/2] Trim the Ohio medical invariants test to ten simulations The invariants do not vary by year, so run Hypothesis on 2022 and 2025 with four examples and the seeded population on 2024 only. Test time falls from about 50 s to 15 s locally; both bugs still fail it. Co-Authored-By: Claude Opus 5.5 --- ..._oh_unreimbursed_medical_deduction_invariants.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py b/policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py index 2f4e8067ebb..e1be0da4318 100644 --- a/policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py +++ b/policyengine_us/tests/core/test_oh_unreimbursed_medical_deduction_invariants.py @@ -28,7 +28,6 @@ """ import numpy as np -import pytest from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st @@ -39,7 +38,10 @@ # Worksheet line 7 multiplies line 6 by 7.5% in every year's instructions # (2022 p. 27, 2023 p. 40, 2024 p. 41, 2025 p. 41). PUBLISHED_RATE = 0.075 -YEARS = [2022, 2024, 2025] +# Nothing here varies by year, so a few years suffice: each simulation costs +# far more than the checks on it. +YEARS = [2022, 2025] +SEEDED_YEAR = 2024 MEDICAL = [ "other_medical_expenses", @@ -260,7 +262,7 @@ def _check(units, year): # A batch's cost is mostly per-variable overhead, so each example is a large # batch and there are few examples. SETTINGS = dict( - max_examples=6, + max_examples=4, deadline=None, derandomize=True, suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], @@ -273,6 +275,5 @@ def test_oh_unreimbursed_medical_deduction_invariants(year, units): _check(units, year) -@pytest.mark.parametrize("year", YEARS) -def test_seeded_population(year): - _check(_seeded_units(), year) +def test_seeded_population(): + _check(_seeded_units(), SEEDED_YEAR)