diff --git a/changelog.d/medicare-part-b-premium-uprating.fixed.md b/changelog.d/medicare-part-b-premium-uprating.fixed.md new file mode 100644 index 00000000000..9fdda55b809 --- /dev/null +++ b/changelog.d/medicare-part-b-premium-uprating.fixed.md @@ -0,0 +1 @@ +Project Medicare Part A, B and D premiums, the Part A deductible, and the Part B and D income-related adjustment amounts and thresholds past CMS's last announced year, using the 2026 Medicare Trustees Report and the statutory inflation adjustment, instead of holding them at their 2026 values. diff --git a/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_a_deductible.yaml b/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_a_deductible.yaml new file mode 100644 index 00000000000..9bfc9ee64b6 --- /dev/null +++ b/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_a_deductible.yaml @@ -0,0 +1,42 @@ +description: The Centers for Medicare and Medicaid Services (CMS) sets, and the Medicare Trustees project, this Medicare Part A inpatient hospital deductible per benefit period. +metadata: + unit: currency-USD + period: year + label: Medicare Part A inpatient hospital deductible + reference: + # Table V.E1, "HI Cost-Sharing and Premium Amounts", "Inpatient hospital + # deductible" column (file page 212): historical amounts through 2026 and + # intermediate estimates for 2027-2035. Report transmitted 2026-06-09, + # retrieved 2026-10-11, sha256 + # ffa56b9137006872300b0346149eae1613d09a172b6ba118aad48e66dfc48fa8. + - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table V.E1 + href: https://www.cms.gov/oact/tr/2026#page=212 + - title: 2026 Medicare Parts A & B Premiums and Deductibles + href: https://www.cms.gov/newsroom/fact-sheets/2026-medicare-parts-b-premiums-deductibles +values: + # Historical. + 2015-01-01: 1_260 + 2016-01-01: 1_288 + 2017-01-01: 1_316 + 2018-01-01: 1_340 + 2019-01-01: 1_364 + 2020-01-01: 1_408 + 2021-01-01: 1_484 + 2022-01-01: 1_556 + 2023-01-01: 1_600 + 2024-01-01: 1_632 + 2025-01-01: 1_676 + 2026-01-01: 1_736 + # Trustees' intermediate estimates. + 2027-01-01: 1_788 + 2028-01-01: 1_844 + 2029-01-01: 1_900 + 2030-01-01: 1_960 + 2031-01-01: 2_028 + 2032-01-01: 2_092 + 2033-01-01: 2_160 + 2034-01-01: 2_228 + 2035-01-01: 2_296 + # 2036 to 2100 grow at the 2034-2035 rate (2,296 / 2,228, 3.1% a year) and + # later years hold the 2100 value; see set_all_uprating_parameters in + # uprating_extensions.py. diff --git a/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_a_premium_per_enrollee.yaml b/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_a_premium_per_enrollee.yaml new file mode 100644 index 00000000000..3d9df232ac0 --- /dev/null +++ b/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_a_premium_per_enrollee.yaml @@ -0,0 +1,42 @@ +description: The Centers for Medicare and Medicaid Services (CMS) sets, and the Medicare Trustees project, this standard monthly Medicare Part A premium per enrollee who has fewer than 30 quarters of Medicare-covered employment. +metadata: + unit: currency-USD + period: month + label: Medicare Part A standard monthly premium per enrollee + reference: + # Table V.E1, "HI Cost-Sharing and Premium Amounts", "Monthly premium, + # Standard" column (file page 212): historical amounts through 2026 and + # intermediate estimates for 2027-2035. Report transmitted 2026-06-09, + # retrieved 2026-10-11, sha256 + # ffa56b9137006872300b0346149eae1613d09a172b6ba118aad48e66dfc48fa8. + - title: 2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table V.E1 + href: https://www.cms.gov/oact/tr/2026#page=212 + - title: 2026 Medicare Parts A & B Premiums and Deductibles + href: https://www.cms.gov/newsroom/fact-sheets/2026-medicare-parts-b-premiums-deductibles +values: + # Historical. + 2015-01-01: 407 + 2016-01-01: 411 + 2017-01-01: 413 + 2018-01-01: 422 + 2019-01-01: 437 + 2020-01-01: 458 + 2021-01-01: 471 + 2022-01-01: 499 + 2023-01-01: 506 + 2024-01-01: 505 + 2025-01-01: 518 + 2026-01-01: 565 + # Trustees' intermediate estimates. + 2027-01-01: 600 + 2028-01-01: 629 + 2029-01-01: 660 + 2030-01-01: 694 + 2031-01-01: 730 + 2032-01-01: 767 + 2033-01-01: 812 + 2034-01-01: 862 + 2035-01-01: 900 + # 2036 to 2100 grow at the 2034-2035 rate (900 / 862, 4.4% a year) and later + # years hold the 2100 value; see set_all_uprating_parameters in + # uprating_extensions.py. diff --git a/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_d_base_beneficiary_premium_per_enrollee.yaml b/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_d_base_beneficiary_premium_per_enrollee.yaml new file mode 100644 index 00000000000..9ad76fe90c3 --- /dev/null +++ b/policyengine_us/parameters/calibration/gov/hhs/cms/medicare_part_d_base_beneficiary_premium_per_enrollee.yaml @@ -0,0 +1,49 @@ +description: The Centers for Medicare and Medicaid Services (CMS) sets, and the Medicare Trustees project, this Medicare Part D base beneficiary premium per enrollee. +metadata: + unit: currency-USD + period: month + label: Medicare Part D base beneficiary premium per enrollee + reference: + # Table V.E2, "SMI Cost-Sharing and Premium Amounts", "Part D, Base + # beneficiary premium" column (file page 213): historical amounts through + # 2026 and intermediate estimates for 2027-2035. The 2030 jump comes from + # the end of the 6 percent cap on yearly growth (42 U.S.C. 1395w-113(a)(8)): + # from 2030 the premium is the (a)(9) percent of the national average + # monthly bid amount and catastrophic reinsurance, and the Trustees expect + # that percent's 20 percent floor to bind (file page 154). Report + # transmitted 2026-06-09, retrieved 2026-10-11, sha256 + # ffa56b9137006872300b0346149eae1613d09a172b6ba118aad48e66dfc48fa8. + - title: "2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, Table V.E2 and Part D premiums #page 154,213" + href: https://www.cms.gov/oact/tr/2026#page=154 + # "For 2027, the base beneficiary premium will be $41.33." Fact sheet of + # July 28, 2026. + - title: Medicare Part D 2027 National Average Monthly Bid Amount Information + href: https://www.cms.gov/newsroom/fact-sheets/medicare-part-d-2027-national-average-monthly-bid-amount-information +values: + # Historical. + 2015-01-01: 33.13 + 2016-01-01: 34.10 + 2017-01-01: 35.63 + 2018-01-01: 35.02 + 2019-01-01: 33.19 + 2020-01-01: 32.74 + 2021-01-01: 33.06 + 2022-01-01: 33.37 + 2023-01-01: 32.74 + 2024-01-01: 34.70 + 2025-01-01: 36.78 + 2026-01-01: 38.99 + # Announced by CMS, equal to the Trustees' intermediate estimate. + 2027-01-01: 41.33 + # Trustees' intermediate estimates. + 2028-01-01: 43.81 + 2029-01-01: 46.44 + 2030-01-01: 68.93 + 2031-01-01: 71.27 + 2032-01-01: 71.59 + 2033-01-01: 74.01 + 2034-01-01: 75.48 + 2035-01-01: 77.81 + # 2036 to 2100 grow at the 2034-2035 rate (77.81 / 75.48, 3.1% a year) and + # later years hold the 2100 value; see set_all_uprating_parameters in + # uprating_extensions.py. diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_a/deductible.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_a/deductible.yaml index 8ab953a004b..ce76572fb60 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_a/deductible.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_a/deductible.yaml @@ -3,6 +3,12 @@ metadata: unit: currency-USD period: year label: Medicare Part A deductible + # After the last announced year, the deductible follows the Medicare + # Trustees' projected deductible (calibration.gov.hhs.cms.medicare_part_a_deductible), + # rounded to the nearest multiple of $4 with a midpoint rounded up, as 42 + # U.S.C. 1395e(b)(1) requires. extend_medicare_part_a_deductible in + # uprating_extensions.py projects it, because core's uprating rounding + # sends a midpoint to the even multiple. reference: - title: 42 U.S.C. § 1395e(b)(1) href: https://www.law.cornell.edu/uscode/text/42/1395e#b_1 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_a/full_premium.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_a/full_premium.yaml index abed6e706aa..f44f34068d1 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_a/full_premium.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_a/full_premium.yaml @@ -3,6 +3,12 @@ metadata: unit: currency-USD period: month label: Medicare Part A full premium + # After the last announced year, the premium grows with the Medicare + # Trustees' projected standard Part A premium + # (calibration.gov.hhs.cms.medicare_part_a_premium_per_enrollee), rounded to + # the nearest dollar with 50-cent amounts rounded up, as 42 U.S.C. + # 1395i-2(d)(2) requires; see extend_medicare_part_a_premiums in + # uprating_extensions.py. reference: - title: 42 U.S.C. § 1395i-2(d) href: https://www.law.cornell.edu/uscode/text/42/1395i-2#d diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_a/reduced_premium.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_a/reduced_premium.yaml index c8f48027e52..cf797a42af7 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_a/reduced_premium.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_a/reduced_premium.yaml @@ -3,6 +3,10 @@ metadata: unit: currency-USD period: month label: Medicare Part A reduced premium + # After the last announced year, the premium is the projected full premium + # reduced by 45 percent, as 42 U.S.C. 1395i-2(d)(4) sets it, rounded to the + # nearest dollar with 50-cent amounts rounded up, as CMS publishes it; see + # extend_medicare_part_a_premiums in uprating_extensions.py. reference: - title: 42 U.S.C. § 1395i-2(d) href: https://www.law.cornell.edu/uscode/text/42/1395i-2#d diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_b/base_premium.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_b/base_premium.yaml index 7f8620f5233..c12eb51ef1b 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_b/base_premium.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_b/base_premium.yaml @@ -3,6 +3,14 @@ metadata: unit: currency-USD period: month label: Medicare Part B base premium + # After the last announced year, the premium follows the Medicare Trustees' + # projected standard premium, rounded to the nearest 10 cents as 42 U.S.C. + # 1395r(c) requires. + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 reference: - title: Medicare Part B premium rates beginning January 1, 2015 (79 FR 61314) href: https://www.govinfo.gov/content/pkg/FR-2014-10-10/pdf/2014-24248.pdf#page=1 @@ -22,6 +30,8 @@ metadata: href: https://www.govinfo.gov/content/pkg/FR-2021-11-17/pdf/2021-25050.pdf#page=1 - title: Medicare Part B premium rates beginning January 1, 2023 (87 FR 59080) href: https://www.govinfo.gov/content/pkg/FR-2022-09-29/pdf/2022-21090.pdf#page=1 + - title: 42 U.S.C. § 1395r(a)(3) and (c) + href: https://www.law.cornell.edu/uscode/text/42/1395r#a_3 - title: 2021 Medicare Parts A & B Premiums and Deductibles href: https://www.cms.gov/newsroom/press-releases/2021-medicare-part-b-premiums-remain-steady - title: 2022 Medicare Parts A & B Premiums and Deductibles diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/head_of_household.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/head_of_household.yaml index 9c5325eb32a..21bc1d6ffb9 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/head_of_household.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/head_of_household.yaml @@ -5,6 +5,12 @@ metadata: amount_unit: currency-USD period: month label: Medicare Part B income brackets for head of household filers + # After the last announced year, each income-related adjustment amount + # follows the Medicare Trustees' projected standard Part B premium: 42 + # U.S.C. 1395r(i)(3)(A) makes each a fixed multiple of the same + # unsubsidized premium amount. The thresholds are indexed to CPI-U under + # 1395r(i)(5); see extend_medicare_irmaa_thresholds in + # uprating_extensions.py. reference: - title: 42 U.S.C. § 1395r(i) href: https://www.law.cornell.edu/uscode/text/42/1395r#i @@ -59,18 +65,25 @@ brackets: 2025-01-01: 106_001 2026-01-01: 109_001 amount: - 2015-01-01: 42.00 - 2016-01-01: 48.70 - 2017-01-01: 53.50 - 2018-01-01: 53.50 - 2019-01-01: 54.10 - 2020-01-01: 57.80 - 2021-01-01: 59.40 - 2022-01-01: 68.00 - 2023-01-01: 65.90 - 2024-01-01: 69.90 - 2025-01-01: 74 - 2026-01-01: 81.20 + values: + 2015-01-01: 42.00 + 2016-01-01: 48.70 + 2017-01-01: 53.50 + 2018-01-01: 53.50 + 2019-01-01: 54.10 + 2020-01-01: 57.80 + 2021-01-01: 59.40 + 2022-01-01: 68.00 + 2023-01-01: 65.90 + 2024-01-01: 69.90 + 2025-01-01: 74 + 2026-01-01: 81.20 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2015-01-01: 107_001 2016-01-01: 107_001 @@ -85,18 +98,25 @@ brackets: 2025-01-01: 133_001 2026-01-01: 137_001 amount: - 2015-01-01: 104.90 - 2016-01-01: 121.80 - 2017-01-01: 133.90 - 2018-01-01: 133.90 - 2019-01-01: 135.40 - 2020-01-01: 144.60 - 2021-01-01: 148.50 - 2022-01-01: 170.10 - 2023-01-01: 164.80 - 2024-01-01: 174.70 - 2025-01-01: 185 - 2026-01-01: 202.90 + values: + 2015-01-01: 104.90 + 2016-01-01: 121.80 + 2017-01-01: 133.90 + 2018-01-01: 133.90 + 2019-01-01: 135.40 + 2020-01-01: 144.60 + 2021-01-01: 148.50 + 2022-01-01: 170.10 + 2023-01-01: 164.80 + 2024-01-01: 174.70 + 2025-01-01: 185 + 2026-01-01: 202.90 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2015-01-01: 160_001 2016-01-01: 160_001 @@ -111,18 +131,25 @@ brackets: 2025-01-01: 167_001 2026-01-01: 171_001 amount: - 2015-01-01: 167.80 - 2016-01-01: 194.90 - 2017-01-01: 214.30 - 2018-01-01: 214.30 - 2019-01-01: 216.70 - 2020-01-01: 231.40 - 2021-01-01: 237.60 - 2022-01-01: 272.20 - 2023-01-01: 263.70 - 2024-01-01: 279.50 - 2025-01-01: 295.9 - 2026-01-01: 324.60 + values: + 2015-01-01: 167.80 + 2016-01-01: 194.90 + 2017-01-01: 214.30 + 2018-01-01: 214.30 + 2019-01-01: 216.70 + 2020-01-01: 231.40 + 2021-01-01: 237.60 + 2022-01-01: 272.20 + 2023-01-01: 263.70 + 2024-01-01: 279.50 + 2025-01-01: 295.9 + 2026-01-01: 324.60 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2015-01-01: 214_001 2016-01-01: 214_001 @@ -137,18 +164,25 @@ brackets: 2025-01-01: 200_001 2026-01-01: 205_001 amount: - 2015-01-01: 230.80 - 2016-01-01: 268.00 - 2017-01-01: 294.60 - 2018-01-01: 294.60 - 2019-01-01: 297.90 - 2020-01-01: 318.10 - 2021-01-01: 326.70 - 2022-01-01: 374.20 - 2023-01-01: 362.60 - 2024-01-01: 384.30 - 2025-01-01: 406.9 - 2026-01-01: 446.30 + values: + 2015-01-01: 230.80 + 2016-01-01: 268.00 + 2017-01-01: 294.60 + 2018-01-01: 294.60 + 2019-01-01: 297.90 + 2020-01-01: 318.10 + 2021-01-01: 326.70 + 2022-01-01: 374.20 + 2023-01-01: 362.60 + 2024-01-01: 384.30 + 2025-01-01: 406.9 + 2026-01-01: 446.30 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: # The 85% tier begins in 2019 (Bipartisan Budget Act of 2018, section 53114). 2015-01-01: .inf @@ -164,16 +198,23 @@ brackets: 2025-01-01: 500_000 2026-01-01: 500_000 amount: - # Before 2019 the threshold is unreachable and the amount repeats the 80% tier. - 2015-01-01: 230.80 - 2016-01-01: 268.00 - 2017-01-01: 294.60 - 2018-01-01: 294.60 - 2019-01-01: 325.00 - 2020-01-01: 347.00 - 2021-01-01: 356.40 - 2022-01-01: 408.20 - 2023-01-01: 395.60 - 2024-01-01: 419.30 - 2025-01-01: 443.9 - 2026-01-01: 487 + values: + # Before 2019 the threshold is unreachable and the amount repeats the 80% tier. + 2015-01-01: 230.80 + 2016-01-01: 268.00 + 2017-01-01: 294.60 + 2018-01-01: 294.60 + 2019-01-01: 325.00 + 2020-01-01: 347.00 + 2021-01-01: 356.40 + 2022-01-01: 408.20 + 2023-01-01: 395.60 + 2024-01-01: 419.30 + 2025-01-01: 443.9 + 2026-01-01: 487 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/joint.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/joint.yaml index 2658cebc16c..eff5404a573 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/joint.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/joint.yaml @@ -5,6 +5,12 @@ metadata: period: month type: single_amount label: Medicare Part B income brackets for joint filers + # After the last announced year, each income-related adjustment amount + # follows the Medicare Trustees' projected standard Part B premium: 42 + # U.S.C. 1395r(i)(3)(A) makes each a fixed multiple of the same + # unsubsidized premium amount. The thresholds are indexed to CPI-U under + # 1395r(i)(5); see extend_medicare_irmaa_thresholds in + # uprating_extensions.py. reference: - title: 42 U.S.C. § 1395r(i) href: https://www.law.cornell.edu/uscode/text/42/1395r#i @@ -59,18 +65,25 @@ brackets: 2025-01-01: 212_001 2026-01-01: 218_001 amount: - 2015-01-01: 42.00 - 2016-01-01: 48.70 - 2017-01-01: 53.50 - 2018-01-01: 53.50 - 2019-01-01: 54.10 - 2020-01-01: 57.80 - 2021-01-01: 59.40 - 2022-01-01: 68.00 - 2023-01-01: 65.90 - 2024-01-01: 69.90 - 2025-01-01: 74 - 2026-01-01: 81.20 + values: + 2015-01-01: 42.00 + 2016-01-01: 48.70 + 2017-01-01: 53.50 + 2018-01-01: 53.50 + 2019-01-01: 54.10 + 2020-01-01: 57.80 + 2021-01-01: 59.40 + 2022-01-01: 68.00 + 2023-01-01: 65.90 + 2024-01-01: 69.90 + 2025-01-01: 74 + 2026-01-01: 81.20 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2015-01-01: 214_001 2016-01-01: 214_001 @@ -85,18 +98,25 @@ brackets: 2025-01-01: 266_001 2026-01-01: 274_001 amount: - 2015-01-01: 104.90 - 2016-01-01: 121.80 - 2017-01-01: 133.90 - 2018-01-01: 133.90 - 2019-01-01: 135.40 - 2020-01-01: 144.60 - 2021-01-01: 148.50 - 2022-01-01: 170.10 - 2023-01-01: 164.80 - 2024-01-01: 174.70 - 2025-01-01: 185 - 2026-01-01: 202.90 + values: + 2015-01-01: 104.90 + 2016-01-01: 121.80 + 2017-01-01: 133.90 + 2018-01-01: 133.90 + 2019-01-01: 135.40 + 2020-01-01: 144.60 + 2021-01-01: 148.50 + 2022-01-01: 170.10 + 2023-01-01: 164.80 + 2024-01-01: 174.70 + 2025-01-01: 185 + 2026-01-01: 202.90 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2015-01-01: 320_001 2016-01-01: 320_001 @@ -111,18 +131,25 @@ brackets: 2025-01-01: 334_001 2026-01-01: 342_001 amount: - 2015-01-01: 167.80 - 2016-01-01: 194.90 - 2017-01-01: 214.30 - 2018-01-01: 214.30 - 2019-01-01: 216.70 - 2020-01-01: 231.40 - 2021-01-01: 237.60 - 2022-01-01: 272.20 - 2023-01-01: 263.70 - 2024-01-01: 279.50 - 2025-01-01: 295.9 - 2026-01-01: 324.60 + values: + 2015-01-01: 167.80 + 2016-01-01: 194.90 + 2017-01-01: 214.30 + 2018-01-01: 214.30 + 2019-01-01: 216.70 + 2020-01-01: 231.40 + 2021-01-01: 237.60 + 2022-01-01: 272.20 + 2023-01-01: 263.70 + 2024-01-01: 279.50 + 2025-01-01: 295.9 + 2026-01-01: 324.60 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2015-01-01: 428_001 2016-01-01: 428_001 @@ -137,18 +164,25 @@ brackets: 2025-01-01: 400_001 2026-01-01: 410_001 amount: - 2015-01-01: 230.80 - 2016-01-01: 268.00 - 2017-01-01: 294.60 - 2018-01-01: 294.60 - 2019-01-01: 297.90 - 2020-01-01: 318.10 - 2021-01-01: 326.70 - 2022-01-01: 374.20 - 2023-01-01: 362.60 - 2024-01-01: 384.30 - 2025-01-01: 406.9 - 2026-01-01: 446.30 + values: + 2015-01-01: 230.80 + 2016-01-01: 268.00 + 2017-01-01: 294.60 + 2018-01-01: 294.60 + 2019-01-01: 297.90 + 2020-01-01: 318.10 + 2021-01-01: 326.70 + 2022-01-01: 374.20 + 2023-01-01: 362.60 + 2024-01-01: 384.30 + 2025-01-01: 406.9 + 2026-01-01: 446.30 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: # The 85% tier begins in 2019 (Bipartisan Budget Act of 2018, section 53114). 2015-01-01: .inf @@ -164,16 +198,23 @@ brackets: 2025-01-01: 750_000 2026-01-01: 750_000 amount: - # Before 2019 the threshold is unreachable and the amount repeats the 80% tier. - 2015-01-01: 230.80 - 2016-01-01: 268.00 - 2017-01-01: 294.60 - 2018-01-01: 294.60 - 2019-01-01: 325.00 - 2020-01-01: 347.00 - 2021-01-01: 356.40 - 2022-01-01: 408.20 - 2023-01-01: 395.60 - 2024-01-01: 419.30 - 2025-01-01: 443.9 - 2026-01-01: 487 + values: + # Before 2019 the threshold is unreachable and the amount repeats the 80% tier. + 2015-01-01: 230.80 + 2016-01-01: 268.00 + 2017-01-01: 294.60 + 2018-01-01: 294.60 + 2019-01-01: 325.00 + 2020-01-01: 347.00 + 2021-01-01: 356.40 + 2022-01-01: 408.20 + 2023-01-01: 395.60 + 2024-01-01: 419.30 + 2025-01-01: 443.9 + 2026-01-01: 487 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/separate.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/separate.yaml index 5b240c9c80d..5aec092072a 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/separate.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/separate.yaml @@ -5,6 +5,12 @@ metadata: period: month type: single_amount label: Medicare Part B income brackets for married filing separately + # After the last announced year, each income-related adjustment amount + # follows the Medicare Trustees' projected standard Part B premium: 42 + # U.S.C. 1395r(i)(3)(A) makes each a fixed multiple of the same + # unsubsidized premium amount. The thresholds are indexed to CPI-U under + # 1395r(i)(5); see extend_medicare_irmaa_thresholds in + # uprating_extensions.py. reference: - title: 42 U.S.C. § 1395r(i) href: https://www.law.cornell.edu/uscode/text/42/1395r#i @@ -60,18 +66,25 @@ brackets: 2025-01-01: 106_001 2026-01-01: 109_001 amount: - 2015-01-01: 167.80 - 2016-01-01: 194.90 - 2017-01-01: 214.30 - 2018-01-01: 294.60 - 2019-01-01: 297.90 - 2020-01-01: 318.10 - 2021-01-01: 326.70 - 2022-01-01: 374.20 - 2023-01-01: 362.60 - 2024-01-01: 384.30 - 2025-01-01: 406.9 - 2026-01-01: 446.30 + values: + 2015-01-01: 167.80 + 2016-01-01: 194.90 + 2017-01-01: 214.30 + 2018-01-01: 294.60 + 2019-01-01: 297.90 + 2020-01-01: 318.10 + 2021-01-01: 326.70 + 2022-01-01: 374.20 + 2023-01-01: 362.60 + 2024-01-01: 384.30 + 2025-01-01: 406.9 + 2026-01-01: 446.30 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: # The 80% tier before 2018 and the 85% tier from 2019. Married filing # separately reduces each individual dollar amount by the $85,000 @@ -90,16 +103,23 @@ brackets: 2025-01-01: 394_000 2026-01-01: 391_000 amount: - # 2018 repeats the 80% tier amount; its threshold is unreachable. - 2015-01-01: 230.80 - 2016-01-01: 268.00 - 2017-01-01: 294.60 - 2018-01-01: 294.60 - 2019-01-01: 325.00 - 2020-01-01: 347.00 - 2021-01-01: 356.40 - 2022-01-01: 408.20 - 2023-01-01: 395.60 - 2024-01-01: 419.30 - 2025-01-01: 443.9 - 2026-01-01: 487 + values: + # 2018 repeats the 80% tier amount; its threshold is unreachable. + 2015-01-01: 230.80 + 2016-01-01: 268.00 + 2017-01-01: 294.60 + 2018-01-01: 294.60 + 2019-01-01: 325.00 + 2020-01-01: 347.00 + 2021-01-01: 356.40 + 2022-01-01: 408.20 + 2023-01-01: 395.60 + 2024-01-01: 419.30 + 2025-01-01: 443.9 + 2026-01-01: 487 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/single.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/single.yaml index 3d518f35958..9d4a5d8201b 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/single.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/single.yaml @@ -5,6 +5,12 @@ metadata: period: month type: single_amount label: Medicare Part B income brackets for single filers + # After the last announced year, each income-related adjustment amount + # follows the Medicare Trustees' projected standard Part B premium: 42 + # U.S.C. 1395r(i)(3)(A) makes each a fixed multiple of the same + # unsubsidized premium amount. The thresholds are indexed to CPI-U under + # 1395r(i)(5); see extend_medicare_irmaa_thresholds in + # uprating_extensions.py. reference: - title: 42 U.S.C. § 1395r(i) href: https://www.law.cornell.edu/uscode/text/42/1395r#i @@ -59,18 +65,25 @@ brackets: 2025-01-01: 106_001 2026-01-01: 109_001 amount: - 2015-01-01: 42.00 - 2016-01-01: 48.70 - 2017-01-01: 53.50 - 2018-01-01: 53.50 - 2019-01-01: 54.10 - 2020-01-01: 57.80 - 2021-01-01: 59.40 - 2022-01-01: 68.00 - 2023-01-01: 65.90 - 2024-01-01: 69.90 - 2025-01-01: 74 - 2026-01-01: 81.20 + values: + 2015-01-01: 42.00 + 2016-01-01: 48.70 + 2017-01-01: 53.50 + 2018-01-01: 53.50 + 2019-01-01: 54.10 + 2020-01-01: 57.80 + 2021-01-01: 59.40 + 2022-01-01: 68.00 + 2023-01-01: 65.90 + 2024-01-01: 69.90 + 2025-01-01: 74 + 2026-01-01: 81.20 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2015-01-01: 107_001 2016-01-01: 107_001 @@ -85,18 +98,25 @@ brackets: 2025-01-01: 133_001 2026-01-01: 137_001 amount: - 2015-01-01: 104.90 - 2016-01-01: 121.80 - 2017-01-01: 133.90 - 2018-01-01: 133.90 - 2019-01-01: 135.40 - 2020-01-01: 144.60 - 2021-01-01: 148.50 - 2022-01-01: 170.10 - 2023-01-01: 164.80 - 2024-01-01: 174.70 - 2025-01-01: 185 - 2026-01-01: 202.90 + values: + 2015-01-01: 104.90 + 2016-01-01: 121.80 + 2017-01-01: 133.90 + 2018-01-01: 133.90 + 2019-01-01: 135.40 + 2020-01-01: 144.60 + 2021-01-01: 148.50 + 2022-01-01: 170.10 + 2023-01-01: 164.80 + 2024-01-01: 174.70 + 2025-01-01: 185 + 2026-01-01: 202.90 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2015-01-01: 160_001 2016-01-01: 160_001 @@ -111,18 +131,25 @@ brackets: 2025-01-01: 167_001 2026-01-01: 171_001 amount: - 2015-01-01: 167.80 - 2016-01-01: 194.90 - 2017-01-01: 214.30 - 2018-01-01: 214.30 - 2019-01-01: 216.70 - 2020-01-01: 231.40 - 2021-01-01: 237.60 - 2022-01-01: 272.20 - 2023-01-01: 263.70 - 2024-01-01: 279.50 - 2025-01-01: 295.9 - 2026-01-01: 324.60 + values: + 2015-01-01: 167.80 + 2016-01-01: 194.90 + 2017-01-01: 214.30 + 2018-01-01: 214.30 + 2019-01-01: 216.70 + 2020-01-01: 231.40 + 2021-01-01: 237.60 + 2022-01-01: 272.20 + 2023-01-01: 263.70 + 2024-01-01: 279.50 + 2025-01-01: 295.9 + 2026-01-01: 324.60 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2015-01-01: 214_001 2016-01-01: 214_001 @@ -137,18 +164,25 @@ brackets: 2025-01-01: 200_001 2026-01-01: 205_001 amount: - 2015-01-01: 230.80 - 2016-01-01: 268.00 - 2017-01-01: 294.60 - 2018-01-01: 294.60 - 2019-01-01: 297.90 - 2020-01-01: 318.10 - 2021-01-01: 326.70 - 2022-01-01: 374.20 - 2023-01-01: 362.60 - 2024-01-01: 384.30 - 2025-01-01: 406.9 - 2026-01-01: 446.30 + values: + 2015-01-01: 230.80 + 2016-01-01: 268.00 + 2017-01-01: 294.60 + 2018-01-01: 294.60 + 2019-01-01: 297.90 + 2020-01-01: 318.10 + 2021-01-01: 326.70 + 2022-01-01: 374.20 + 2023-01-01: 362.60 + 2024-01-01: 384.30 + 2025-01-01: 406.9 + 2026-01-01: 446.30 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: # The 85% tier begins in 2019 (Bipartisan Budget Act of 2018, section 53114). 2015-01-01: .inf @@ -164,16 +198,23 @@ brackets: 2025-01-01: 500_000 2026-01-01: 500_000 amount: - # Before 2019 the threshold is unreachable and the amount repeats the 80% tier. - 2015-01-01: 230.80 - 2016-01-01: 268.00 - 2017-01-01: 294.60 - 2018-01-01: 294.60 - 2019-01-01: 325.00 - 2020-01-01: 347.00 - 2021-01-01: 356.40 - 2022-01-01: 408.20 - 2023-01-01: 395.60 - 2024-01-01: 419.30 - 2025-01-01: 443.9 - 2026-01-01: 487 + values: + # Before 2019 the threshold is unreachable and the amount repeats the 80% tier. + 2015-01-01: 230.80 + 2016-01-01: 268.00 + 2017-01-01: 294.60 + 2018-01-01: 294.60 + 2019-01-01: 325.00 + 2020-01-01: 347.00 + 2021-01-01: 356.40 + 2022-01-01: 408.20 + 2023-01-01: 395.60 + 2024-01-01: 419.30 + 2025-01-01: 443.9 + 2026-01-01: 487 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/surviving_spouse.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/surviving_spouse.yaml index a2454f151c7..6dabe856dab 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/surviving_spouse.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_b/irmaa/surviving_spouse.yaml @@ -5,6 +5,12 @@ metadata: type: single_amount period: month label: Medicare Part B income brackets for surviving spouse filers + # After the last announced year, each income-related adjustment amount + # follows the Medicare Trustees' projected standard Part B premium: 42 + # U.S.C. 1395r(i)(3)(A) makes each a fixed multiple of the same + # unsubsidized premium amount. The thresholds are indexed to CPI-U under + # 1395r(i)(5); see extend_medicare_irmaa_thresholds in + # uprating_extensions.py. reference: - title: 42 U.S.C. § 1395r(i) href: https://www.law.cornell.edu/uscode/text/42/1395r#i @@ -59,18 +65,25 @@ brackets: 2025-01-01: 106_001 2026-01-01: 109_001 amount: - 2015-01-01: 42.00 - 2016-01-01: 48.70 - 2017-01-01: 53.50 - 2018-01-01: 53.50 - 2019-01-01: 54.10 - 2020-01-01: 57.80 - 2021-01-01: 59.40 - 2022-01-01: 68.00 - 2023-01-01: 65.90 - 2024-01-01: 69.90 - 2025-01-01: 74 - 2026-01-01: 81.20 + values: + 2015-01-01: 42.00 + 2016-01-01: 48.70 + 2017-01-01: 53.50 + 2018-01-01: 53.50 + 2019-01-01: 54.10 + 2020-01-01: 57.80 + 2021-01-01: 59.40 + 2022-01-01: 68.00 + 2023-01-01: 65.90 + 2024-01-01: 69.90 + 2025-01-01: 74 + 2026-01-01: 81.20 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2015-01-01: 107_001 2016-01-01: 107_001 @@ -85,18 +98,25 @@ brackets: 2025-01-01: 133_001 2026-01-01: 137_001 amount: - 2015-01-01: 104.90 - 2016-01-01: 121.80 - 2017-01-01: 133.90 - 2018-01-01: 133.90 - 2019-01-01: 135.40 - 2020-01-01: 144.60 - 2021-01-01: 148.50 - 2022-01-01: 170.10 - 2023-01-01: 164.80 - 2024-01-01: 174.70 - 2025-01-01: 185 - 2026-01-01: 202.90 + values: + 2015-01-01: 104.90 + 2016-01-01: 121.80 + 2017-01-01: 133.90 + 2018-01-01: 133.90 + 2019-01-01: 135.40 + 2020-01-01: 144.60 + 2021-01-01: 148.50 + 2022-01-01: 170.10 + 2023-01-01: 164.80 + 2024-01-01: 174.70 + 2025-01-01: 185 + 2026-01-01: 202.90 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2015-01-01: 160_001 2016-01-01: 160_001 @@ -111,18 +131,25 @@ brackets: 2025-01-01: 167_001 2026-01-01: 171_001 amount: - 2015-01-01: 167.80 - 2016-01-01: 194.90 - 2017-01-01: 214.30 - 2018-01-01: 214.30 - 2019-01-01: 216.70 - 2020-01-01: 231.40 - 2021-01-01: 237.60 - 2022-01-01: 272.20 - 2023-01-01: 263.70 - 2024-01-01: 279.50 - 2025-01-01: 295.9 - 2026-01-01: 324.60 + values: + 2015-01-01: 167.80 + 2016-01-01: 194.90 + 2017-01-01: 214.30 + 2018-01-01: 214.30 + 2019-01-01: 216.70 + 2020-01-01: 231.40 + 2021-01-01: 237.60 + 2022-01-01: 272.20 + 2023-01-01: 263.70 + 2024-01-01: 279.50 + 2025-01-01: 295.9 + 2026-01-01: 324.60 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: 2015-01-01: 214_001 2016-01-01: 214_001 @@ -137,18 +164,25 @@ brackets: 2025-01-01: 200_001 2026-01-01: 205_001 amount: - 2015-01-01: 230.80 - 2016-01-01: 268.00 - 2017-01-01: 294.60 - 2018-01-01: 294.60 - 2019-01-01: 297.90 - 2020-01-01: 318.10 - 2021-01-01: 326.70 - 2022-01-01: 374.20 - 2023-01-01: 362.60 - 2024-01-01: 384.30 - 2025-01-01: 406.9 - 2026-01-01: 446.30 + values: + 2015-01-01: 230.80 + 2016-01-01: 268.00 + 2017-01-01: 294.60 + 2018-01-01: 294.60 + 2019-01-01: 297.90 + 2020-01-01: 318.10 + 2021-01-01: 326.70 + 2022-01-01: 374.20 + 2023-01-01: 362.60 + 2024-01-01: 384.30 + 2025-01-01: 406.9 + 2026-01-01: 446.30 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 - threshold: # The 85% tier begins in 2019 (Bipartisan Budget Act of 2018, section 53114). 2015-01-01: .inf @@ -164,16 +198,23 @@ brackets: 2025-01-01: 500_000 2026-01-01: 500_000 amount: - # Before 2019 the threshold is unreachable and the amount repeats the 80% tier. - 2015-01-01: 230.80 - 2016-01-01: 268.00 - 2017-01-01: 294.60 - 2018-01-01: 294.60 - 2019-01-01: 325.00 - 2020-01-01: 347.00 - 2021-01-01: 356.40 - 2022-01-01: 408.20 - 2023-01-01: 395.60 - 2024-01-01: 419.30 - 2025-01-01: 443.9 - 2026-01-01: 487 + values: + # Before 2019 the threshold is unreachable and the amount repeats the 80% tier. + 2015-01-01: 230.80 + 2016-01-01: 268.00 + 2017-01-01: 294.60 + 2018-01-01: 294.60 + 2019-01-01: 325.00 + 2020-01-01: 347.00 + 2021-01-01: 356.40 + 2022-01-01: 408.20 + 2023-01-01: 395.60 + 2024-01-01: 419.30 + 2025-01-01: 443.9 + 2026-01-01: 487 + metadata: + uprating: + parameter: calibration.gov.hhs.cms.medicare_part_b_premium_per_enrollee + rounding: + type: nearest + interval: 0.1 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/base_beneficiary_premium_percent.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/base_beneficiary_premium_percent.yaml new file mode 100644 index 00000000000..424cd0e776c --- /dev/null +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/base_beneficiary_premium_percent.yaml @@ -0,0 +1,21 @@ +description: The Centers for Medicare & Medicaid Services (CMS) uses this percent to set the Medicare Part D base beneficiary premium and to scale the Part D income-related monthly adjustment amount. +metadata: + unit: /1 + period: year + label: Medicare Part D base beneficiary premium percent + reference: + # (a)(3)(A) and (a)(7)(B)(i): 25.5 percent, or from 2030 the percent + # specified under (a)(9). (a)(9) sets that percent so the 2030 base + # beneficiary premium equals the lesser of the 2029 premium increased by 6 + # percent or the premium without (a)(9), but not below 20 percent. + - title: 42 U.S.C. § 1395w-113(a)(3), (a)(7)(B) and (a)(9) + href: https://www.law.cornell.edu/uscode/text/42/1395w-113#a + # Footnote 63: "Currently, we expect the percentage in 2030 will be set at + # 20 percent, causing a substantial premium increase." + - title: "2026 Annual Report of the Boards of Trustees of the Federal Hospital Insurance and Federal Supplementary Medical Insurance Trust Funds, footnote 63 and Part D premiums #page 113,154" + href: https://www.cms.gov/oact/tr/2026#page=113 +values: + 2006-01-01: 0.255 + # The Trustees' expectation of the (a)(9) percent, which is also its floor; + # CMS determines it for 2030. + 2030-01-01: 0.2 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/head_of_household.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/head_of_household.yaml index 2ec7622348a..7ef2e5166d2 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/head_of_household.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/head_of_household.yaml @@ -5,6 +5,11 @@ metadata: period: month type: single_amount label: Medicare Part D IRMAA brackets for head of household filers + # After the last announced year, the thresholds are indexed to CPI-U under + # 42 U.S.C. 1395r(i)(5) and each adjustment amount is computed from the + # Medicare Trustees' projected base beneficiary premium under 42 U.S.C. + # 1395w-113(a)(7)(B); see extend_medicare_irmaa_thresholds and + # extend_medicare_part_d_irmaa_amounts in uprating_extensions.py. reference: - title: 42 U.S.C. § 1395w-113(a)(7) - Part D income-related premium adjustment. href: https://www.law.cornell.edu/uscode/text/42/1395w-113#a_7 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/joint.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/joint.yaml index f4fd1fa50df..e7919bc0884 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/joint.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/joint.yaml @@ -5,6 +5,11 @@ metadata: period: month type: single_amount label: Medicare Part D IRMAA brackets for joint filers + # After the last announced year, the thresholds are indexed to CPI-U under + # 42 U.S.C. 1395r(i)(5) and each adjustment amount is computed from the + # Medicare Trustees' projected base beneficiary premium under 42 U.S.C. + # 1395w-113(a)(7)(B); see extend_medicare_irmaa_thresholds and + # extend_medicare_part_d_irmaa_amounts in uprating_extensions.py. reference: - title: 42 U.S.C. § 1395w-113(a)(7) - Part D income-related premium adjustment. href: https://www.law.cornell.edu/uscode/text/42/1395w-113#a_7 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/separate.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/separate.yaml index 864f5e25ec0..e28563fb50d 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/separate.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/separate.yaml @@ -5,6 +5,11 @@ metadata: period: month type: single_amount label: Medicare Part D IRMAA brackets for married filing separately + # After the last announced year, the thresholds are indexed to CPI-U under + # 42 U.S.C. 1395r(i)(5) and each adjustment amount is computed from the + # Medicare Trustees' projected base beneficiary premium under 42 U.S.C. + # 1395w-113(a)(7)(B); see extend_medicare_irmaa_thresholds and + # extend_medicare_part_d_irmaa_amounts in uprating_extensions.py. reference: - title: 42 U.S.C. § 1395w-113(a)(7) - Part D income-related premium adjustment. href: https://www.law.cornell.edu/uscode/text/42/1395w-113#a_7 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/single.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/single.yaml index c16ed7011bf..f495cc23ce1 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/single.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/single.yaml @@ -5,6 +5,11 @@ metadata: period: month type: single_amount label: Medicare Part D IRMAA brackets for single filers + # After the last announced year, the thresholds are indexed to CPI-U under + # 42 U.S.C. 1395r(i)(5) and each adjustment amount is computed from the + # Medicare Trustees' projected base beneficiary premium under 42 U.S.C. + # 1395w-113(a)(7)(B); see extend_medicare_irmaa_thresholds and + # extend_medicare_part_d_irmaa_amounts in uprating_extensions.py. reference: - title: 42 U.S.C. § 1395w-113(a)(7) - Part D income-related premium adjustment. href: https://www.law.cornell.edu/uscode/text/42/1395w-113#a_7 diff --git a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/surviving_spouse.yaml b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/surviving_spouse.yaml index 17b24e21693..1d71c2e67d6 100644 --- a/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/surviving_spouse.yaml +++ b/policyengine_us/parameters/gov/hhs/medicare/part_d/irmaa/surviving_spouse.yaml @@ -5,6 +5,11 @@ metadata: period: month type: single_amount label: Medicare Part D IRMAA brackets for surviving spouse filers + # After the last announced year, the thresholds are indexed to CPI-U under + # 42 U.S.C. 1395r(i)(5) and each adjustment amount is computed from the + # Medicare Trustees' projected base beneficiary premium under 42 U.S.C. + # 1395w-113(a)(7)(B); see extend_medicare_irmaa_thresholds and + # extend_medicare_part_d_irmaa_amounts in uprating_extensions.py. reference: - title: 42 U.S.C. § 1395w-113(a)(7) - Part D income-related premium adjustment. href: https://www.law.cornell.edu/uscode/text/42/1395w-113#a_7 diff --git a/policyengine_us/parameters/uprating_extensions.py b/policyengine_us/parameters/uprating_extensions.py index 960aa4482f3..6a174142225 100644 --- a/policyengine_us/parameters/uprating_extensions.py +++ b/policyengine_us/parameters/uprating_extensions.py @@ -864,6 +864,303 @@ def extend_wa_millionaires_standard_deduction( ) +# 42 U.S.C. 1395i-2(d)(4): an individual with 30 to 39 quarters of coverage +# pays the Part A premium reduced by 45 percent. +MEDICARE_PART_A_REDUCED_PREMIUM_SHARE = 0.55 + + +def round_half_up(value: float, interval: float) -> float: + """Round ``value`` to the nearest multiple of ``interval``, sending an + amount midway between two multiples to the higher one, as 42 U.S.C. + 1395i-2(d)(2) does for the Part A premium and 1395e(b)(1) for the Part A + deductible.""" + step = Decimal(repr(interval)) + return float( + (Decimal(repr(value)) / step).quantize(Decimal(1), rounding=ROUND_HALF_UP) + * step + ) + + +def extend_medicare_part_a_premiums(parameters: ParameterNode, end_year: int) -> None: + """Project the Medicare Part A full and reduced monthly premiums. + + Each year after the last CMS-published one, the full premium is the last + published premium grown with the Medicare Trustees' projected standard + premium, rounded to the nearest dollar with 50-cent amounts rounded up + (42 U.S.C. 1395i-2(d)(2)). The reduced premium is the full premium less + 45 percent (1395i-2(d)(4)), rounded the same way; CMS's published + reduced premiums follow that rule in every year from 2015 to 2026. CMS + values encoded in the YAML take precedence for their own years. + """ + part_a = parameters.gov.hhs.medicare.part_a + series = parameters.calibration.gov.hhs.cms.medicare_part_a_premium_per_enrollee + full, reduced = part_a.full_premium, part_a.reduced_premium + + def last_published_year(parameter: Parameter) -> int: + return max(int(value.instant_str[:4]) for value in parameter.values_list) + + last_full_year = last_published_year(full) + last_reduced_year = last_published_year(reduced) + base_period = f"{last_full_year}-01-01" + scale = full(base_period) / series(base_period) + for year in range(last_full_year + 1, end_year + 1): + period = f"{year}-01-01" + full_premium = round_half_up(scale * series(period), 1) + full.update(period=f"year:{period}:1", value=full_premium) + for year in range(last_reduced_year + 1, end_year + 1): + period = f"{year}-01-01" + reduced.update( + period=f"year:{period}:1", + value=round_half_up( + MEDICARE_PART_A_REDUCED_PREMIUM_SHARE * full(period), 1 + ), + ) + for parameter in (full, reduced): + parameter.update( + start=instant(f"{end_year}-01-01"), + value=parameter(f"{end_year}-01-01"), + ) + + +def extend_medicare_part_a_deductible(parameters: ParameterNode, end_year: int) -> None: + """Project the Medicare Part A inpatient hospital deductible. + + Each year after the last CMS-published one, the deductible is the last + published deductible grown with the Medicare Trustees' projected + deductible, rounded to the nearest multiple of $4. An amount midway + between two multiples goes to the higher one (42 U.S.C. 1395e(b)(1)), + which core's ``nearest`` uprating rounding would send to the even + multiple. CMS values encoded in the YAML take precedence for their own + years. + """ + deductible = parameters.gov.hhs.medicare.part_a.deductible + series = parameters.calibration.gov.hhs.cms.medicare_part_a_deductible + last_year = max(int(value.instant_str[:4]) for value in deductible.values_list) + base_period = f"{last_year}-01-01" + scale = deductible(base_period) / series(base_period) + for year in range(last_year + 1, end_year + 1): + period = f"{year}-01-01" + deductible.update( + period=f"year:{period}:1", + value=round_half_up(scale * series(period), 4), + ) + deductible.update( + start=instant(f"{end_year}-01-01"), + value=deductible(f"{end_year}-01-01"), + ) + + +# 42 U.S.C. 1395r(i)(3)(C)(i)(III): the dollar amounts of the applicable +# percentage table for an unmarried individual, before the (5) inflation +# adjustment. The first is also the (2)(A) threshold amount. +MEDICARE_IRMAA_STATUTORY_AMOUNTS = (85_000, 107_000, 133_500, 160_000) +MEDICARE_IRMAA_STATUTORY_TOP_AMOUNT = 500_000 +# 1395r(i)(5)(A): for 2020 on, CPI-U for the 12 months ending August of the +# preceding year over the 12 months ending August 2018. (5)(C): the $500,000 +# amount is not adjusted before 2028, and from 2028 is adjusted against the 12 +# months ending August 2026. +MEDICARE_IRMAA_CPI_BASE_YEAR = 2018 +MEDICARE_IRMAA_FIRST_INDEXED_YEAR = 2020 +MEDICARE_IRMAA_TOP_CPI_BASE_YEAR = 2026 +MEDICARE_IRMAA_TOP_FIRST_INDEXED_YEAR = 2028 +# 1395r(i)(3)(C)(i)(III): the applicable percentage of each adjusted tier, by +# bracket of each filing status's scale (the first bracket pays no +# adjustment). A married individual filing separately who lives with the +# spouse pays an adjustment only above the unmarried threshold amount +# ((i)(1)), which is above where (C)(iii) starts the 80 percent tier, so that +# scale has only the 80 and 85 percent tiers. +MEDICARE_IRMAA_APPLICABLE_PERCENTAGES = { + "single": (0.35, 0.50, 0.65, 0.80, 0.85), + "joint": (0.35, 0.50, 0.65, 0.80, 0.85), + "head_of_household": (0.35, 0.50, 0.65, 0.80, 0.85), + "surviving_spouse": (0.35, 0.50, 0.65, 0.80, 0.85), + "separate": (0.80, 0.85), +} + + +def round_medicare_irmaa_amount(amount: float) -> float: + """Round an adjusted dollar amount to the nearest multiple of $1,000. + + 42 U.S.C. 1395r(i)(5)(B) names no rule for an amount midway between two + multiples; it rounds up here. + """ + return float( + (Decimal(repr(amount)) / 1_000).quantize(Decimal(1), rounding=ROUND_HALF_UP) + * 1_000 + ) + + +def get_medicare_irmaa_unmarried_amounts( + parameters: ParameterNode, year: int +) -> Tuple[Tuple[float, ...], float]: + """The unmarried dollar amounts of the IRMAA table for ``year``. + + Returns the four amounts above which the 35, 50, 65 and 80 percent tiers + begin, and the amount at which the 85 percent tier begins, each adjusted + under 42 U.S.C. 1395r(i)(5): increased by the percentage (if any) by which + average CPI-U for the 12 months ending August of the preceding year + exceeds its base-period average, then rounded to the nearest $1,000. + """ + if year < MEDICARE_IRMAA_FIRST_INDEXED_YEAR: + raise ValueError( + f"1395r(i)(5) adjusts against August 2018 only from " + f"{MEDICARE_IRMAA_FIRST_INDEXED_YEAR}; got {year}." + ) + window = get_cpi_u_average_for_12_months_ending_august(parameters, year - 1) + + def adjust(amount: int, base_year: int) -> float: + base = get_cpi_u_average_for_12_months_ending_august(parameters, base_year) + return round_medicare_irmaa_amount(amount * (1 + max(window / base - 1, 0))) + + lower = tuple( + adjust(amount, MEDICARE_IRMAA_CPI_BASE_YEAR) + for amount in MEDICARE_IRMAA_STATUTORY_AMOUNTS + ) + if year < MEDICARE_IRMAA_TOP_FIRST_INDEXED_YEAR: + top = float(MEDICARE_IRMAA_STATUTORY_TOP_AMOUNT) + else: + top = adjust( + MEDICARE_IRMAA_STATUTORY_TOP_AMOUNT, MEDICARE_IRMAA_TOP_CPI_BASE_YEAR + ) + return lower, top + + +def get_medicare_irmaa_thresholds( + lower: Tuple[float, ...], top: float +) -> Dict[str, Tuple[float, ...]]: + """Bracket thresholds of each filing status's IRMAA scale. + + The first four tiers cover income "more than" a dollar amount and the top + tier income of "at least" its amount (42 U.S.C. 1395r(i)(3)(C)(i)(III)). + A bracket applies from its threshold, so, as in the published years, the + first four thresholds sit $1 above their amounts. + """ + if lower[3] - lower[0] >= lower[0]: + raise ValueError( + "The married filing separately scale assumes its 80 percent tier " + "starts below the unmarried threshold amount." + ) + unmarried = (0, *(amount + 1 for amount in lower), top) + return { + "single": unmarried, + "head_of_household": unmarried, + "surviving_spouse": unmarried, + # (C)(ii): twice the unmarried amounts for the calendar year, but 150 percent + # of the unmarried top amount for the calendar year (the $500,000 amount as + # adjusted under (5)(C)). Under (5)(B), the Secretary rounds only amounts + # increased under (A) or (C). + "joint": (0, *(2 * amount + 1 for amount in lower), 1.5 * top), + # (C)(iii): each amount less the unmarried threshold amount, with no + # adjustment at or below that threshold ((i)(1)). + "separate": (0, lower[0] + 1, top - lower[0]), + } + + +def extend_medicare_irmaa_thresholds(parameters: ParameterNode, end_year: int) -> None: + """Project the Medicare Part B and Part D IRMAA income thresholds. + + Part D uses the Part B thresholds (42 U.S.C. 1395w-113(a)(7)(A) and + (B)(i)(I)). Each year after the last CMS-published one is computed from + the statutory amounts (see ``get_medicare_irmaa_unmarried_amounts``) + rather than chained from the prior year's rounded values. CMS values + encoded in the YAML take precedence for their own years. + """ + medicare = parameters.gov.hhs.medicare + thresholds_by_year = {} + for program in (medicare.part_b, medicare.part_d): + for name, scale in program.irmaa.children.items(): + if name not in MEDICARE_IRMAA_APPLICABLE_PERCENTAGES: + raise ValueError(f"Unexpected Medicare IRMAA scale: {name}.") + for index, bracket in enumerate(scale.brackets): + threshold = bracket.threshold + first_projected_year = 1 + max( + int(value.instant_str[:4]) for value in threshold.values_list + ) + for year in range(first_projected_year, end_year + 1): + if year not in thresholds_by_year: + thresholds_by_year[year] = get_medicare_irmaa_thresholds( + *get_medicare_irmaa_unmarried_amounts(parameters, year) + ) + year_thresholds = thresholds_by_year[year][name] + if len(year_thresholds) != len(scale.brackets): + raise ValueError( + f"Expected {len(year_thresholds)} brackets in the " + f"{name} Medicare IRMAA scale; found " + f"{len(scale.brackets)}." + ) + threshold.update( + period=f"year:{year}-01-01:1", + value=float(year_thresholds[index]), + ) + threshold.update( + start=instant(f"{end_year}-01-01"), + value=threshold(f"{end_year}-01-01"), + ) + + +def get_medicare_part_d_irmaa_amount( + base_beneficiary_premium: float, + applicable_percentage: float, + base_percent: float, +) -> float: + """One Part D income-related monthly adjustment amount. + + 42 U.S.C. 1395w-113(a)(7)(B): the base beneficiary premium times the + applicable percentage less the base beneficiary premium percent, divided + by that percent. CMS publishes the amounts to the nearest 10 cents. + """ + premium = Decimal(repr(base_beneficiary_premium)) + applicable = Decimal(repr(applicable_percentage)) + base = Decimal(repr(base_percent)) + amount = premium * (applicable - base) / base + return float(amount.quantize(Decimal("0.1"), rounding=ROUND_HALF_UP)) + + +def extend_medicare_part_d_irmaa_amounts( + parameters: ParameterNode, end_year: int +) -> None: + """Project the Medicare Part D income-related monthly adjustment amounts. + + Each year after the last CMS-published one applies 42 U.S.C. + 1395w-113(a)(7)(B) to the Medicare Trustees' projected base beneficiary + premium. The base beneficiary premium percent falls from 25.5 percent to + the (a)(9) percent in 2030, which raises every amount; the Trustees + expect 20 percent. CMS values encoded in the YAML take precedence for + their own years. + """ + base_beneficiary_premium = parameters.calibration.gov.hhs.cms.medicare_part_d_base_beneficiary_premium_per_enrollee + part_d = parameters.gov.hhs.medicare.part_d + for name, scale in part_d.irmaa.children.items(): + applicable_percentages = MEDICARE_IRMAA_APPLICABLE_PERCENTAGES.get(name) + if applicable_percentages is None or len(applicable_percentages) != len( + scale.brackets[1:] + ): + raise ValueError( + f"Unexpected Medicare Part D IRMAA scale or bracket count: {name}." + ) + for bracket, applicable_percentage in zip( + scale.brackets[1:], applicable_percentages + ): + amount = bracket.amount + first_projected_year = 1 + max( + int(value.instant_str[:4]) for value in amount.values_list + ) + for year in range(first_projected_year, end_year + 1): + period = f"{year}-01-01" + amount.update( + period=f"year:{period}:1", + value=get_medicare_part_d_irmaa_amount( + base_beneficiary_premium(period), + applicable_percentage, + part_d.base_beneficiary_premium_percent(period), + ), + ) + amount.update( + start=instant(f"{end_year}-01-01"), + value=amount(f"{end_year}-01-01"), + ) + + def long_range_growth_rate( year: int, last_projected_year: int, @@ -1350,6 +1647,43 @@ def set_all_uprating_parameters(parameters: ParameterNode) -> ParameterNode: period_day=1, ) + # Medicare Part A standard premium and inpatient hospital deductible, and + # Part D base beneficiary premium (January values, last Trustees + # projection year 2035). The Part A premiums, the deductible and the Part + # D income-related adjustment amounts are computed from them below. + for medicare_series in ( + parameters.calibration.gov.hhs.cms.medicare_part_a_premium_per_enrollee, + parameters.calibration.gov.hhs.cms.medicare_part_a_deductible, + parameters.calibration.gov.hhs.cms.medicare_part_d_base_beneficiary_premium_per_enrollee, + ): + extend_parameter_values( + medicare_series, + last_projected_year=2035, + end_year=END_YEAR, + period_month=1, + period_day=1, + ) + + # Medicare Part A full and reduced premiums follow the Trustees' projected + # standard premium extended above, with the reduced premium 45 percent + # below the full one (42 U.S.C. 1395i-2(d)). + extend_medicare_part_a_premiums(parameters, end_year=END_YEAR) + + # The Medicare Part A deductible follows the Trustees' projected + # deductible extended above, rounded to $4 with midpoints rounded up + # (42 U.S.C. 1395e(b)(1)). + extend_medicare_part_a_deductible(parameters, end_year=END_YEAR) + + # Medicare IRMAA income thresholds follow 42 U.S.C. 1395r(i)(5)'s CPI-U + # schedule (12 months ending August), computed from the statutory + # amounts. Must run after the CPI-U extension above. + extend_medicare_irmaa_thresholds(parameters, end_year=END_YEAR) + + # Medicare Part D income-related adjustment amounts follow 42 U.S.C. + # 1395w-113(a)(7)(B) applied to the base beneficiary premium extended + # above. + extend_medicare_part_d_irmaa_amounts(parameters, end_year=END_YEAR) + # Gross Social Security benefit inputs should age with gross Social # Security benefits, not taxable Social Security income. extend_parameter_values( diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index 95ca1cfdbfe..0f74596fd68 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -1827,6 +1827,9 @@ programs: Includes Part A (hospital) and Part B (medical) premiums with IRMAA adjustments; gross Part B premium calibration should use the enrollment-gated gross_medicare_part_b_premium_if_enrolled variable. + After CMS's last announced year, premiums and IRMAA amounts follow the + Medicare Trustees' projections and IRMAA thresholds the statutory CPI-U + indexing. # --- HUD programs --- - id: section_8 diff --git a/policyengine_us/tests/core/test_medicare_premium_projection.py b/policyengine_us/tests/core/test_medicare_premium_projection.py new file mode 100644 index 00000000000..464726fabb8 --- /dev/null +++ b/policyengine_us/tests/core/test_medicare_premium_projection.py @@ -0,0 +1,762 @@ +"""Medicare premiums, deductible and IRMAA after CMS's last announced year. + +CMS announces each year's Part A and B premiums, Part A deductible and +income-related monthly adjustment amounts (IRMAA) in the autumn before. For +later years the model projects them: + +- The Part B standard premium and Part B IRMAA amounts uprate by the + Medicare Trustees' projected standard premium, rounded to 10 cents. +- The Part A full premium grows with the Trustees' projected standard + premium, rounded to $1 with 50-cent amounts rounded up; the reduced premium + is the full premium less 45 percent, rounded the same way. The deductible + uprates by the Trustees' projection, rounded to $4. +- The IRMAA income thresholds follow 42 U.S.C. 1395r(i)(5)'s CPI-U schedule. +- The Part D IRMAA amounts apply 42 U.S.C. 1395w-113(a)(7)(B) to the + Trustees' projected base beneficiary premium. + +These tests read the parameter tree, plus small parameter trees built for +the precedence tests. Household calculations are in the YAML tests under +``tests/policy/baseline/gov/hhs/medicare``. + +When CMS announces a year, add its amounts to the YAML and move +``LAST_ANNOUNCED_YEAR`` forward. Later years then grow from the announced +amounts, so the Trustees rows pinned below for those years no longer apply +exactly; update them from the next Trustees Report. + +Invariants: + +1. Source agreement (differential, model against published figures): for + 2027-2035, the projections equal the 2026 Trustees Report's Part B + premium (Table V.E2), Part A premiums and deductible (Table V.E1) and + Part D IRMAA amounts (Table V.E4). The Part B IRMAA amounts are within + $0.30 of Table V.E3 through 2034 and $0.80 in 2035, where Table V.E3 + implies a $360.90 standard premium against Table V.E2's $360.60. +2. Statutory rounding: in every projected year, the Part B premium and IRMAA + amounts are multiples of 10 cents, the Part A premiums of $1 and the + deductible of $4. A deductible midway between two multiples of $4 goes to + the higher one (42 U.S.C. 1395e(b)(1)), shown on a small series because + no Trustees year lands on a midpoint. +3. Agreement with the uprater: the Part B premium equals the Trustees series + rounded to 10 cents in every year from 2021. +4. Proportionality: each Part B IRMAA amount stays at its 42 U.S.C. + 1395r(i)(3)(A) multiple of the standard premium, to within 0.1% plus + rounding, as in CMS's 2026 amounts. Each Part D amount equals + 1395w-113(a)(7)(B) applied to the base beneficiary premium. In every + year from 2015, published or projected, the reduced Part A premium is the + full premium less 45 percent (1395i-2(d)(4)), rounded to the nearest + dollar with 50-cent amounts rounded up, and each projected full premium + is the Trustees series scaled to the last published premium, rounded the + same way (1395i-2(d)(2)). +5. Monotonicity: the Part B premium rises every year from 2023, and the Part + A premiums and deductible never fall from 2024. IRMAA thresholds never + fall from one year to the next and rise from tier to tier. Intended + exception: the married-filing-separately top threshold is $500,000 less + the rising unmarried threshold, so it falls each year until the $500,000 + amount is indexed in 2028. +6. Threshold structure: joint thresholds are twice the unmarried amounts + (150% for the top tier), married filing separately uses the unmarried + threshold and the top amount less that threshold, and Part D uses the Part + B thresholds. The top tier stays at $500,000 through 2027. +7. Indexing (differential): the statutory CPI-U schedule reproduces CMS's + published thresholds exactly for 2020-2024, where the model's CPI-U is + the published not seasonally adjusted index, and within one $1,000 step + for 2025-2026, where it is seasonally adjusted. A fall in CPI-U leaves + every amount unadjusted (the percentage "(if any)" in (5)(A) and + (C)(ii)), shown on a small falling CPI-U path. +8. Precedence: a CMS amount added to the YAML for a year replaces the + projection for that year only. Later IRMAA thresholds and Part D amounts + are still computed from the statute rather than chained from it; later + Part A premiums grow from it. Each Part A premium is projected from its + own last published year. +9. Extension: the new Trustees series grow at their last one-year rate past + 2035 and hold their 2100 value after 2100. +""" + +import math +from types import SimpleNamespace + +import pytest +from hypothesis import given +from hypothesis import strategies as st +from policyengine_core.parameters import ParameterNode +from policyengine_core.parameters.operations.get_parameter import get_parameter + +from policyengine_us.parameters.uprating_extensions import ( + MEDICARE_IRMAA_APPLICABLE_PERCENTAGES, + MEDICARE_IRMAA_TOP_FIRST_INDEXED_YEAR, + extend_medicare_irmaa_thresholds, + extend_medicare_part_a_deductible, + extend_medicare_part_a_premiums, + extend_medicare_part_d_irmaa_amounts, + get_cpi_u_average_for_12_months_ending_august, + get_medicare_irmaa_thresholds, + get_medicare_irmaa_unmarried_amounts, + get_medicare_part_d_irmaa_amount, + round_half_up, +) +from policyengine_us.system import system + +PARAMETERS = system.parameters +MEDICARE = PARAMETERS.gov.hhs.medicare +CMS = "calibration.gov.hhs.cms" +PART_B_SERIES = f"{CMS}.medicare_part_b_premium_per_enrollee" +PART_A_SERIES = f"{CMS}.medicare_part_a_premium_per_enrollee" +DEDUCTIBLE_SERIES = f"{CMS}.medicare_part_a_deductible" +PART_D_SERIES = f"{CMS}.medicare_part_d_base_beneficiary_premium_per_enrollee" +LAST_ANNOUNCED_YEAR = 2026 +LAST_TRUSTEES_YEAR = 2035 +END_YEAR = 2100 +FILING_STATUSES = ("single", "joint", "head_of_household", "surviving_spouse") +ALL_STATUSES = (*FILING_STATUSES, "separate") +PROJECTED_YEARS = st.integers(LAST_ANNOUNCED_YEAR + 1, END_YEAR + 20) +TRUSTEES_YEARS = range(LAST_ANNOUNCED_YEAR + 1, LAST_TRUSTEES_YEAR + 1) + + +def _at(parameter, year): + return parameter(f"{year}-01-01") + + +def _series(path, year): + return _at(get_parameter(PARAMETERS, path), year) + + +def _is_multiple(value, step): + return abs(value / step - round(value / step)) < 1e-6 + + +# 2026 Medicare Trustees Report (transmitted 2026-06-09, sha256 +# ffa56b91...8fa8), intermediate estimates for 2027-2035. +# Table V.E1 (file page 212): inpatient hospital deductible, standard and +# reduced monthly Part A premiums. +TRUSTEES_PART_A_DEDUCTIBLE = dict( + zip( + range(2026, 2036), + (1_736, 1_788, 1_844, 1_900, 1_960, 2_028, 2_092, 2_160, 2_228, 2_296), + ) +) +TRUSTEES_PART_A_FULL_PREMIUM = dict( + zip(TRUSTEES_YEARS, (600, 629, 660, 694, 730, 767, 812, 862, 900)) +) +TRUSTEES_PART_A_REDUCED_PREMIUM = dict( + zip(TRUSTEES_YEARS, (330, 346, 363, 382, 402, 422, 447, 474, 495)) +) +# Table V.E2 (file page 213): standard monthly Part B premium and Part D base +# beneficiary premium. +TRUSTEES_PART_B_PREMIUM = dict( + zip( + TRUSTEES_YEARS, + (209.5, 224.5, 238.5, 255.5, 272.1, 290.2, 313.6, 338.5, 360.6), + ) +) +TRUSTEES_PART_D_BASE_PREMIUM = dict( + zip( + TRUSTEES_YEARS, + (41.33, 43.81, 46.44, 68.93, 71.27, 71.59, 74.01, 75.48, 77.81), + ) +) +# Table V.E3 (file page 215): Part B income-related monthly adjustment +# amounts for the 35, 50, 65, 80 and 85 percent tiers. +TRUSTEES_PART_B_IRMAA = { + 2027: (83.70, 209.40, 335.10, 460.70, 502.60), + 2028: (89.80, 224.50, 359.20, 493.90, 538.80), + 2029: (95.30, 238.40, 381.50, 524.50, 572.20), + 2030: (102.10, 255.40, 408.70, 561.90, 613.00), + 2031: (108.80, 272.10, 435.40, 598.60, 653.00), + 2032: (116.00, 290.10, 464.20, 638.30, 696.30), + 2033: (125.40, 313.60, 501.80, 689.90, 752.60), + 2034: (135.30, 338.40, 541.50, 744.50, 812.20), + 2035: (144.40, 360.90, 577.40, 794.00, 866.20), +} +# Table V.E4 (file page 218): Part D income-related monthly adjustment +# amounts for the same tiers. +TRUSTEES_PART_D_IRMAA = { + 2027: (15.40, 39.70, 64.00, 88.30, 96.40), + 2028: (16.30, 42.10, 67.90, 93.60, 102.20), + 2029: (17.30, 44.60, 71.90, 99.30, 108.40), + 2030: (51.70, 103.40, 155.10, 206.80, 224.00), + 2031: (53.50, 106.90, 160.40, 213.80, 231.60), + 2032: (53.70, 107.40, 161.10, 214.80, 232.70), + 2033: (55.50, 111.00, 166.50, 222.00, 240.50), + 2034: (56.60, 113.20, 169.80, 226.40, 245.30), + 2035: (58.40, 116.70, 175.10, 233.40, 252.90), +} +# 42 U.S.C. 1395r(i)(3)(A): (applicable percentage - 25 points) x (200% of the +# aged actuarial rate + 4 x the (a)(6) premium increase). The standard premium +# is 50% of that rate plus the (a)(6) increase, so before rounding each amount +# is 4 x (applicable percentage - 25 points) times the standard premium. +PART_B_IRMAA_MULTIPLES = (0.4, 1.0, 1.6, 2.2, 2.4) +# CMS fact sheets "2020/2021/2022/2023 Medicare Parts A & B Premiums and +# Deductibles": the unmarried amounts above which the 35, 50, 65 and 80 +# percent tiers begin. 2024-2026 are the YAML's published values. +CMS_UNMARRIED_AMOUNTS = { + 2020: (87_000, 109_000, 136_000, 163_000), + 2021: (88_000, 111_000, 138_000, 165_000), + 2022: (91_000, 114_000, 142_000, 170_000), + 2023: (97_000, 123_000, 153_000, 183_000), + 2024: (103_000, 129_000, 161_000, 193_000), + 2025: (106_000, 133_000, 167_000, 200_000), + 2026: (109_000, 137_000, 171_000, 205_000), +} + + +def _brackets(program, status): + return getattr(program.irmaa, status).brackets + + +def _thresholds(program, status, year): + return [_at(b.threshold, year) for b in _brackets(program, status)] + + +# 1. Source agreement. + + +@pytest.mark.parametrize("year", TRUSTEES_YEARS) +def test_part_b_premium_is_the_trustees_projection(year): + assert _at(MEDICARE.part_b.base_premium, year) == pytest.approx( + TRUSTEES_PART_B_PREMIUM[year], abs=1e-9 + ) + + +@pytest.mark.parametrize("year", TRUSTEES_YEARS) +def test_part_a_premiums_are_the_trustees_projection(year): + assert _at(MEDICARE.part_a.full_premium, year) == pytest.approx( + TRUSTEES_PART_A_FULL_PREMIUM[year], abs=1e-9 + ) + assert _at(MEDICARE.part_a.reduced_premium, year) == pytest.approx( + TRUSTEES_PART_A_REDUCED_PREMIUM[year], abs=1e-9 + ) + + +@pytest.mark.parametrize("year", sorted(TRUSTEES_PART_A_DEDUCTIBLE)) +def test_part_a_deductible_is_the_trustees_figure(year): + """2026 is CMS's announced $1,736; later years are projections.""" + assert _at(MEDICARE.part_a.deductible, year) == pytest.approx( + TRUSTEES_PART_A_DEDUCTIBLE[year], abs=1e-9 + ) + + +@pytest.mark.parametrize("year", TRUSTEES_YEARS) +@pytest.mark.parametrize("status", FILING_STATUSES) +def test_part_b_irmaa_is_within_cents_of_the_trustees_projection(status, year): + tolerance = (0.80 if year == LAST_TRUSTEES_YEAR else 0.30) + 1e-9 + amounts = [_at(b.amount, year) for b in _brackets(MEDICARE.part_b, status)[1:]] + assert amounts == pytest.approx(TRUSTEES_PART_B_IRMAA[year], abs=tolerance) + + +@pytest.mark.parametrize("year", TRUSTEES_YEARS) +@pytest.mark.parametrize("status", FILING_STATUSES) +def test_part_d_irmaa_is_the_trustees_projection(status, year): + """Applying the statute to the Trustees' base beneficiary premium, with + the 20 percent base from 2030, reproduces their Table V.E4.""" + amounts = [_at(b.amount, year) for b in _brackets(MEDICARE.part_d, status)[1:]] + assert amounts == pytest.approx(TRUSTEES_PART_D_IRMAA[year], abs=1e-9) + + +@pytest.mark.parametrize("year", TRUSTEES_YEARS) +def test_married_filing_separately_gets_the_two_top_tiers(year): + for program in (MEDICARE.part_b, MEDICARE.part_d): + separate = [_at(b.amount, year) for b in _brackets(program, "separate")] + single = [_at(b.amount, year) for b in _brackets(program, "single")] + assert separate == [0, *single[-2:]] + + +@pytest.mark.parametrize("year", [2024, 2025, 2026]) +def test_part_d_formula_reproduces_cms_published_amounts(year): + """Differential check of the formula and its rounding against CMS's + published amounts, from CMS's published base beneficiary premiums.""" + base_premium = _series(PART_D_SERIES, year) + for status in ALL_STATUSES: + brackets = _brackets(MEDICARE.part_d, status)[1:] + for bracket, percentage in zip( + brackets, MEDICARE_IRMAA_APPLICABLE_PERCENTAGES[status] + ): + assert get_medicare_part_d_irmaa_amount( + base_premium, percentage, 0.255 + ) == pytest.approx(_at(bracket.amount, year), abs=1e-9) + + +@pytest.mark.parametrize( + "path, values", + [ + (PART_A_SERIES, TRUSTEES_PART_A_FULL_PREMIUM), + (DEDUCTIBLE_SERIES, TRUSTEES_PART_A_DEDUCTIBLE), + (PART_D_SERIES, TRUSTEES_PART_D_BASE_PREMIUM), + ], +) +def test_new_series_transcribe_the_trustees_tables(path, values): + for year, value in values.items(): + assert _series(path, year) == value + + +@pytest.mark.parametrize("year", range(2021, LAST_ANNOUNCED_YEAR + 1)) +def test_part_a_series_agrees_with_announced_premiums(year): + assert _series(PART_A_SERIES, year) == _at(MEDICARE.part_a.full_premium, year) + + +@pytest.mark.parametrize("year", range(2024, LAST_ANNOUNCED_YEAR + 1)) +def test_deductible_series_agrees_with_announced_deductibles(year): + assert _series(DEDUCTIBLE_SERIES, year) == _at(MEDICARE.part_a.deductible, year) + + +# 2. Statutory rounding. + + +@given(PROJECTED_YEARS) +def test_projected_amounts_are_rounded_as_published(year): + assert _is_multiple(_at(MEDICARE.part_b.base_premium, year), 0.1) + assert _is_multiple(_at(MEDICARE.part_a.full_premium, year), 1) + assert _is_multiple(_at(MEDICARE.part_a.reduced_premium, year), 1) + assert _is_multiple(_at(MEDICARE.part_a.deductible, year), 4) + for program in (MEDICARE.part_b, MEDICARE.part_d): + for status in ALL_STATUSES: + for bracket in _brackets(program, status): + assert _is_multiple(_at(bracket.amount, year), 0.1) + + +# 3. Agreement with the uprater. + + +@given(st.integers(2021, END_YEAR + 20)) +def test_part_b_premium_is_the_series_rounded_to_ten_cents(year): + series = _series(PART_B_SERIES, year) + premium = _at(MEDICARE.part_b.base_premium, year) + assert premium == pytest.approx(round(series / 0.1) * 0.1, abs=1e-9) + assert abs(premium - series) <= 0.05 + 1e-9 + + +# 4. Proportionality. + + +@given(PROJECTED_YEARS, st.sampled_from(FILING_STATUSES)) +def test_part_b_irmaa_keeps_its_statutory_multiple(year, status): + premium = _at(MEDICARE.part_b.base_premium, year) + for bracket, multiple in zip( + _brackets(MEDICARE.part_b, status)[1:], PART_B_IRMAA_MULTIPLES + ): + expected = multiple * premium + assert abs(_at(bracket.amount, year) - expected) <= 0.001 * expected + 0.1 + + +@given(PROJECTED_YEARS, st.sampled_from(ALL_STATUSES)) +def test_part_d_irmaa_follows_the_statute(year, status): + base_premium = _series(PART_D_SERIES, year) + base_percent = _at(MEDICARE.part_d.base_beneficiary_premium_percent, year) + for bracket, percentage in zip( + _brackets(MEDICARE.part_d, status)[1:], + MEDICARE_IRMAA_APPLICABLE_PERCENTAGES[status], + ): + assert _at(bracket.amount, year) == get_medicare_part_d_irmaa_amount( + base_premium, percentage, base_percent + ) + + +def test_part_d_base_percent_resets_in_2030(): + percent = MEDICARE.part_d.base_beneficiary_premium_percent + assert _at(percent, 2029) == 0.255 + assert _at(percent, 2030) == 0.2 + + +@given(st.integers(2015, END_YEAR + 20)) +def test_reduced_part_a_premium_is_55_percent_of_the_full_premium(year): + """1395i-2(d)(4) reduces the premium by 45 percent. CMS's published + amounts follow it with ties rounded up, for example $310.75 to $311 in + 2026, and the projection must keep following it.""" + expected = round_half_up(0.55 * _at(MEDICARE.part_a.full_premium, year), 1) + assert _at(MEDICARE.part_a.reduced_premium, year) == expected + + +@given(PROJECTED_YEARS) +def test_full_part_a_premium_is_the_series_rounded_half_up(year): + """Projected full premiums are the Trustees series scaled to the last + published premium, rounded as 1395i-2(d)(2) requires.""" + scale = _at(MEDICARE.part_a.full_premium, LAST_ANNOUNCED_YEAR) / _series( + PART_A_SERIES, LAST_ANNOUNCED_YEAR + ) + expected = round_half_up(scale * _series(PART_A_SERIES, year), 1) + assert _at(MEDICARE.part_a.full_premium, year) == expected + + +def test_round_half_up_sends_ties_up(): + assert round_half_up(566.5, 1) == 567 + assert round_half_up(310.75, 1) == 311 + assert round_half_up(563.2, 1) == 563 + + +def _deductible_tree(series_values): + """The Part A deductible as loaded from YAML through the last announced + year, under a root whose Trustees deductible series is ``series_values``.""" + values = { + v.instant_str: v.value + for v in MEDICARE.part_a.deductible.values_list + if v.instant_str <= f"{LAST_ANNOUNCED_YEAR}-12-31" + } + part_a = ParameterNode( + "gov.hhs.medicare.part_a", data={"deductible": {"values": values}} + ) + cms = ParameterNode( + CMS, data={"medicare_part_a_deductible": {"values": series_values}} + ) + root = SimpleNamespace( + gov=SimpleNamespace( + hhs=SimpleNamespace(medicare=SimpleNamespace(part_a=part_a)) + ), + calibration=SimpleNamespace(gov=SimpleNamespace(hhs=SimpleNamespace(cms=cms))), + ) + return root, part_a.deductible + + +def test_projected_deductible_rounds_a_midpoint_up(): + """42 U.S.C. 1395e(b)(1) sends an amount midway between two multiples of + $4 to the higher one. No Trustees year lands on a midpoint, so a small + series puts 2027 at $1,738, midway between $1,736 and $1,740, where + rounding half to even would give $1,736.""" + root, deductible = _deductible_tree( + {"2026-01-01": 1_736, "2027-01-01": 1_738, "2028-01-01": 1_745} + ) + extend_medicare_part_a_deductible(root, end_year=2028) + assert _at(deductible, LAST_ANNOUNCED_YEAR) == 1_736 + assert _at(deductible, 2027) == 1_740 + assert _at(deductible, 2028) == 1_744 + assert _at(deductible, 2100) == 1_744 + + +# 5. Monotonicity. + + +@given(st.integers(2023, END_YEAR - 1), st.integers(1, 80)) +def test_part_b_premium_rises_every_year_from_2023(year, gap): + later = min(year + gap, END_YEAR) + assert _at(MEDICARE.part_b.base_premium, year) < _at( + MEDICARE.part_b.base_premium, later + ) + + +@given(st.integers(2024, END_YEAR + 20), st.integers(1, 80)) +def test_part_a_amounts_never_fall_from_2024(year, gap): + for parameter in ( + MEDICARE.part_a.full_premium, + MEDICARE.part_a.reduced_premium, + MEDICARE.part_a.deductible, + ): + assert _at(parameter, year) <= _at(parameter, year + gap) + + +@given(st.integers(2024, END_YEAR + 20), st.integers(1, 80)) +def test_irmaa_thresholds_never_fall(year, gap): + for program in (MEDICARE.part_b, MEDICARE.part_d): + for status in ALL_STATUSES: + brackets = _brackets(program, status) + for bracket in brackets: + if ( + status == "separate" + and bracket is brackets[-1] + and year < MEDICARE_IRMAA_TOP_FIRST_INDEXED_YEAR + ): + continue # The intended exception; see the next test. + assert _at(bracket.threshold, year) <= _at( + bracket.threshold, year + gap + ) + + +@pytest.mark.parametrize("year", range(2025, 2028)) +def test_separate_top_threshold_falls_until_2028(year): + """$500,000 less the unmarried threshold: $397,000 in 2024, $394,000 in + 2025 and $391,000 in 2026 as CMS published, then lower until 2028.""" + top = _brackets(MEDICARE.part_b, "separate")[-1].threshold + assert _at(top, year) < _at(top, year - 1) + single = _thresholds(MEDICARE.part_b, "single", year) + assert _at(top, year) == 500_000 - (single[1] - 1) + + +@given(st.integers(2024, END_YEAR + 20)) +def test_irmaa_tiers_rise_with_income_and_amount(year): + for program in (MEDICARE.part_b, MEDICARE.part_d): + for status in ALL_STATUSES: + brackets = _brackets(program, status) + thresholds = [_at(b.threshold, year) for b in brackets] + amounts = [_at(b.amount, year) for b in brackets] + assert thresholds == sorted(set(thresholds)) + assert amounts == sorted(set(amounts)) + + +# 6. Threshold structure. + + +@given(st.integers(2024, END_YEAR + 20)) +def test_thresholds_follow_the_filing_status_rules(year): + single = _thresholds(MEDICARE.part_b, "single", year) + lower, top = [t - 1 for t in single[1:5]], single[5] + assert _thresholds(MEDICARE.part_b, "head_of_household", year) == single + assert _thresholds(MEDICARE.part_b, "surviving_spouse", year) == single + assert _thresholds(MEDICARE.part_b, "joint", year) == [ + 0, + *(2 * amount + 1 for amount in lower), + 1.5 * top, + ] + assert _thresholds(MEDICARE.part_b, "separate", year) == [ + 0, + lower[0] + 1, + top - lower[0], + ] + for status in ALL_STATUSES: + assert _thresholds(MEDICARE.part_d, status, year) == _thresholds( + MEDICARE.part_b, status, year + ) + + +@given(st.integers(2024, END_YEAR + 20)) +def test_unmarried_amounts_are_whole_thousands(year): + """The first four thresholds sit $1 above a $1,000 multiple ("more + than" the amount); the top tier starts at a $1,000 multiple.""" + single = _thresholds(MEDICARE.part_b, "single", year) + assert all(_is_multiple(t - 1, 1_000) for t in single[1:5]) + assert _is_multiple(single[5], 1_000) + + +@pytest.mark.parametrize("year", range(2024, 2028)) +def test_top_tier_is_not_indexed_before_2028(year): + assert _thresholds(MEDICARE.part_b, "single", year)[5] == 500_000 + assert _thresholds(MEDICARE.part_b, "joint", year)[5] == 750_000 + + +def test_top_tier_is_indexed_from_2028(): + assert _thresholds(MEDICARE.part_b, "single", 2028)[5] > 500_000 + + +@given(st.integers(MEDICARE_IRMAA_TOP_FIRST_INDEXED_YEAR, END_YEAR)) +def test_projected_unmarried_thresholds_use_their_statutory_base_years(year): + """Each projected unmarried threshold is its statutory dollar amount + times CPI-U for the 12 months ending August of the prior year over the + base period's, rounded to the nearest $1,000: August 2018 for the four + lower amounts and August 2026 for the $500,000 amount (42 U.S.C. + 1395r(i)(5)). The amounts and base years are written out here rather + than read from the module, so a wrong base year there fails.""" + + def adjusted(amount, base_year): + ratio = get_cpi_u_average_for_12_months_ending_august( + PARAMETERS, year - 1 + ) / get_cpi_u_average_for_12_months_ending_august(PARAMETERS, base_year) + return math.floor(amount * max(ratio, 1) / 1_000 + 0.5) * 1_000 + + expected = [ + 0, + *(adjusted(a, 2018) + 1 for a in (85_000, 107_000, 133_500, 160_000)), + adjusted(500_000, 2026), + ] + assert _thresholds(MEDICARE.part_b, "single", year) == expected + + +@pytest.mark.parametrize("year", range(2024, LAST_ANNOUNCED_YEAR + 1)) +def test_threshold_rules_reproduce_the_published_scales(year): + """Rebuilding each published year's scales from its unmarried amounts + reproduces every filing status's published thresholds.""" + lower = tuple(float(a) for a in CMS_UNMARRIED_AMOUNTS[year]) + rebuilt = get_medicare_irmaa_thresholds(lower, 500_000.0) + for status in ALL_STATUSES: + assert _thresholds(MEDICARE.part_b, status, year) == list(rebuilt[status]) + + +# 7. Indexing against CMS's published thresholds. + + +@pytest.mark.parametrize("year", sorted(CMS_UNMARRIED_AMOUNTS)) +def test_cpi_schedule_reproduces_published_thresholds(year): + lower, top = get_medicare_irmaa_unmarried_amounts(PARAMETERS, year) + published = CMS_UNMARRIED_AMOUNTS[year] + assert top == 500_000 + if year <= 2024: + assert lower == published + else: + # From January 2024 the model's CPI-U is seasonally adjusted, while + # CMS averages the published index; 2025's 12-month averages differ + # by about 0.05%, enough to move two amounts one rounding step. + assert all(abs(a - b) <= 1_000 for a, b in zip(lower, published)) + + +def test_cpi_schedule_starts_in_2020(): + with pytest.raises(ValueError): + get_medicare_irmaa_unmarried_amounts(PARAMETERS, 2019) + + +def test_amounts_do_not_fall_when_cpi_u_falls(): + """42 U.S.C. 1395r(i)(5)(A) and (C)(ii) increase each amount by the + percentage (if any) by which average CPI-U exceeds its base-period + average, so a fall leaves it unadjusted. The published index has not + fallen below either base, so a small CPI-U path averages 300 for the 12 + months ending August 2018, 270 for those ending August 2026 and 250 for + those ending August 2027.""" + cpi = ParameterNode( + "gov.bls.cpi", + data={ + "cpi_u": { + "values": { + "2017-09-01": 300, + "2018-09-01": 285, + "2025-09-01": 270, + "2026-09-01": 250, + } + } + }, + ) + root = SimpleNamespace(gov=SimpleNamespace(bls=SimpleNamespace(cpi=cpi))) + for year in (2027, 2028): + lower, top = get_medicare_irmaa_unmarried_amounts(root, year) + for adjusted, amount in zip(lower, (85_000, 107_000, 133_500, 160_000)): + # At most the rounding step up to the nearest $1,000. + assert amount <= adjusted < amount + 1_000 + assert top == 500_000 + + +# 8. Precedence of published amounts. + + +def _medicare_tree_with_2027_values(): + """The IRMAA scales as loaded from YAML, with a 2027 value added to each + bracket, under a root that shares the system's CPI-U and Trustees + series.""" + data = {} + for program in ("part_b", "part_d"): + node = getattr(MEDICARE, program).irmaa + data[program] = {"irmaa": {}} + for status, scale in node.children.items(): + brackets = [] + for index, bracket in enumerate(scale.brackets): + entry = {} + for field in ("threshold", "amount"): + parameter = getattr(bracket, field) + values = { + v.instant_str: v.value + for v in parameter.values_list + if v.instant_str <= f"{LAST_ANNOUNCED_YEAR}-12-31" + } + # A stand-in for a CMS announcement: last year's value + # plus $1, which no projection produces. The first tier + # pays no adjustment in any year. + if field == "threshold" or index > 0: + last = values[f"{LAST_ANNOUNCED_YEAR}-01-01"] + values["2027-01-01"] = last + 1 + entry[field] = {"values": values} + brackets.append(entry) + data[program]["irmaa"][status] = { + "metadata": {"type": "single_amount"}, + "brackets": brackets, + } + data["part_d"]["base_beneficiary_premium_percent"] = { + "values": {"2006-01-01": 0.255, "2030-01-01": 0.2} + } + medicare = ParameterNode("gov.hhs.medicare", data=data) + root = SimpleNamespace( + gov=SimpleNamespace( + hhs=SimpleNamespace(medicare=medicare), bls=PARAMETERS.gov.bls + ), + calibration=PARAMETERS.calibration, + ) + return root, medicare + + +def test_published_values_take_precedence_and_later_years_follow_the_statute(): + root, medicare = _medicare_tree_with_2027_values() + extend_medicare_irmaa_thresholds(root, end_year=2030) + extend_medicare_part_d_irmaa_amounts(root, end_year=2030) + for program in ("part_b", "part_d"): + for status in ALL_STATUSES: + built = _brackets(getattr(medicare, program), status) + system_brackets = _brackets(getattr(MEDICARE, program), status) + for index, (bracket, system_bracket) in enumerate( + zip(built, system_brackets) + ): + assert _at(bracket.threshold, 2027) == ( + _at(bracket.threshold, LAST_ANNOUNCED_YEAR) + 1 + ) + if program == "part_d" and index > 0: + assert _at(bracket.amount, 2027) == ( + _at(bracket.amount, LAST_ANNOUNCED_YEAR) + 1 + ) + # 2028 on is computed from the statute, as in the system. + for year in (2028, 2030): + assert _at(bracket.threshold, year) == _at( + system_bracket.threshold, year + ) + if program == "part_d": + assert _at(bracket.amount, year) == _at( + system_bracket.amount, year + ) + + +def _part_a_tree_with_2027_values(full_2027=611, reduced_2027=337): + """The Part A premiums as loaded from YAML through the last announced + year, plus a stand-in CMS announcement for 2027 that no projection + produces (None leaves that premium unannounced), under a root that shares + the system's Trustees series.""" + data = {} + for name in ("full_premium", "reduced_premium"): + parameter = getattr(MEDICARE.part_a, name) + values = { + v.instant_str: v.value + for v in parameter.values_list + if v.instant_str <= f"{LAST_ANNOUNCED_YEAR}-12-31" + } + data[name] = {"values": values} + if full_2027 is not None: + data["full_premium"]["values"]["2027-01-01"] = full_2027 + if reduced_2027 is not None: + data["reduced_premium"]["values"]["2027-01-01"] = reduced_2027 + part_a = ParameterNode("gov.hhs.medicare.part_a", data=data) + root = SimpleNamespace( + gov=SimpleNamespace( + hhs=SimpleNamespace(medicare=SimpleNamespace(part_a=part_a)) + ), + calibration=PARAMETERS.calibration, + ) + return root, part_a + + +def test_published_part_a_premiums_take_precedence_and_later_years_grow_from_them(): + root, part_a = _part_a_tree_with_2027_values() + extend_medicare_part_a_premiums(root, end_year=2030) + assert _at(part_a.full_premium, 2027) == 611 + assert _at(part_a.reduced_premium, 2027) == 337 + for year in (2028, 2030): + full = round_half_up( + 611 / _series(PART_A_SERIES, 2027) * _series(PART_A_SERIES, year), 1 + ) + assert _at(part_a.full_premium, year) == full + assert _at(part_a.reduced_premium, year) == round_half_up(0.55 * full, 1) + assert _at(part_a.full_premium, 2100) == _at(part_a.full_premium, 2030) + + +def test_reduced_part_a_premium_is_projected_from_its_own_last_published_year(): + """A year CMS published for only one of the two premiums keeps that + published value, and the other premium is projected for it.""" + # The full premium is announced for 2027 and the reduced one is not. + root, part_a = _part_a_tree_with_2027_values(reduced_2027=None) + extend_medicare_part_a_premiums(root, end_year=2030) + assert _at(part_a.full_premium, 2027) == 611 + # 0.55 * 611 = 336.05. + assert _at(part_a.reduced_premium, 2027) == 336 + # The reduced premium is announced for 2027 and the full one is not. + root, part_a = _part_a_tree_with_2027_values(full_2027=None) + extend_medicare_part_a_premiums(root, end_year=2030) + assert _at(part_a.reduced_premium, 2027) == 337 + for year in (2028, 2030): + full = _at(part_a.full_premium, year) + assert _at(part_a.reduced_premium, year) == round_half_up(0.55 * full, 1) + + +# 9. Extension of the new series. + +NEW_SERIES = (PART_A_SERIES, DEDUCTIBLE_SERIES, PART_D_SERIES) + + +@given(st.sampled_from(NEW_SERIES), st.integers(LAST_TRUSTEES_YEAR + 1, END_YEAR)) +def test_new_series_extend_at_the_last_trustees_growth_rate(path, year): + last_growth = _series(path, LAST_TRUSTEES_YEAR) / _series( + path, LAST_TRUSTEES_YEAR - 1 + ) + growth = _series(path, year) / _series(path, year - 1) + assert growth == pytest.approx(last_growth, rel=1e-12) + + +@given(st.sampled_from(NEW_SERIES), st.integers(END_YEAR, END_YEAR + 100)) +def test_new_series_hold_their_2100_value(path, year): + assert _series(path, year) == _series(path, END_YEAR) diff --git a/policyengine_us/tests/microsimulation/test_social_security_current_law.py b/policyengine_us/tests/microsimulation/test_social_security_current_law.py index 3f593e1a9a7..04648ec4b9d 100644 --- a/policyengine_us/tests/microsimulation/test_social_security_current_law.py +++ b/policyengine_us/tests/microsimulation/test_social_security_current_law.py @@ -62,6 +62,12 @@ ) AMOUNTS = ((12_000, 0, 1_800, 600), (0, 6_000, 1_200, 300)) REVIEWED_MAIN_SHA = "8986e05cab65d56435a23df1910e2d35f2c5f5bf" +# Requests that differ from REVIEWED_MAIN_SHA for reasons outside Social +# Security ingress. The reference predates the Medicare premium projection, +# so its 2027 Part B premium differs. The medicare_irmaa_magi requests still +# pin the IRMAA ingress path. Remove this entry once REVIEWED_MAIN_SHA +# includes the projection. +CHANGED_SINCE_REFERENCE = {"medicare_part_b_premium@2027"} # Rev. Proc. 2026-35 replaced the 2027 federal forecasts that the reviewed # main used (brackets, standard deduction and other indexed amounts). On these # routes 2027 income tax falls by exactly these amounts and household net @@ -1091,6 +1097,8 @@ def differential(tmp_path_factory): def test_legacy_public_ingress_matches_unmodified_main(differential, route): current, main, _ = differential for request in main[route]: + if request in CHANGED_SINCE_REFERENCE: + continue if request in current[route] and not np.array_equal( current[route][request], main[route][request] ): @@ -1107,6 +1115,8 @@ def test_legacy_public_ingress_matches_unmodified_main(differential, route): ) assert current[route].keys() == main[route].keys() for request in main[route]: + if request in CHANGED_SINCE_REFERENCE: + continue if route in INCOME_TAX_CHANGE_2027 and request in ( "income_tax@2027", "household_net_income@2027", diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_a_premium.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_a_premium.yaml index 08f4a203c53..b8160918e73 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_a_premium.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_a_premium.yaml @@ -179,3 +179,44 @@ medicare_quarters_of_coverage: 20 output: base_part_a_premium: 6_780 # $565 * 12 months + +# Projected years follow the Trustees' Table V.E1. +- name: 2027 projected reduced Part A premium. + period: 2027 + input: + age: 65 + is_medicare_eligible: true + medicare_quarters_of_coverage: 35 + output: + base_part_a_premium: 3_960 # $330 * 12 months + +- name: 2027 projected full Part A premium. + period: 2027 + input: + age: 65 + is_medicare_eligible: true + medicare_quarters_of_coverage: 20 + output: + base_part_a_premium: 7_200 # $600 * 12 months + +# 2038 extends the Trustees' Table V.E1 standard premium at its 2034-2035 +# growth: $900 x (900 / 862)^3 = $1,024.35, rounded to $1,024. The reduced +# premium is $1,024 less 45 percent, $563.20, rounded to $563 (42 U.S.C. +# 1395i-2(d)(4)). +- name: 2038 projected reduced Part A premium. + period: 2038 + input: + age: 65 + is_medicare_eligible: true + medicare_quarters_of_coverage: 35 + output: + base_part_a_premium: 6_756 # $563 * 12 months + +- name: 2038 projected full Part A premium. + period: 2038 + input: + age: 65 + is_medicare_eligible: true + medicare_quarters_of_coverage: 20 + output: + base_part_a_premium: 12_288 # $1,024 * 12 months diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_b_premium.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_b_premium.yaml index c926e088231..04f18e8e662 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_b_premium.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/base_part_b_premium.yaml @@ -91,3 +91,21 @@ output: # Source: 84 FR 61625, standard premium. base_part_b_premium: 1_735.2 # $144.60 * 12 months + +# After CMS's last announced year (2026) the premium follows the 2026 Medicare +# Trustees Report's projected standard premium (Table V.E2). +- name: 2027 projected Part B premium. + period: 2027 + input: + age: 65 + is_medicare_eligible: true + output: + base_part_b_premium: 2_514 # $209.50 * 12 months + +- name: 2034 projected Part B premium. + period: 2034 + input: + age: 65 + is_medicare_eligible: true + output: + base_part_b_premium: 4_062 # $338.50 * 12 months diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/income_adjusted_part_d_premium_surcharge.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/income_adjusted_part_d_premium_surcharge.yaml index 320962069b7..1f826420e3f 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/income_adjusted_part_d_premium_surcharge.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/income_adjusted_part_d_premium_surcharge.yaml @@ -2607,3 +2607,33 @@ # MAGI 403,000, at the top tier: $76.40 (85%) a month # MAGI 2,000,000, far above every threshold: $76.40 (85%) a month income_adjusted_part_d_premium_surcharge: [0, 840, 840, 916.8, 916.8] + +# Projected years: 42 U.S.C. 1395w-113(a)(7)(B) applied to the Trustees' +# projected base beneficiary premium (Table V.E2). The base beneficiary +# premium percent falls from 25.5 to 20 percent in 2030, so the 35 percent +# tier's adjustment jumps from $46.44 * 9.5 / 25.5 = $17.30 to $68.93 * 15 / +# 20 = $51.70, as in the Trustees' Table V.E4. Joint 35 percent tier: $238,001 +# to $300,000 in 2029 and $242,001 to $306,000 in 2030. +- name: 2029 joint filers in the 35 percent tier. + period: 2029 + input: + age: 65 + filing_status: JOINT + adjusted_gross_income: + 2027: 270_000 + tax_exempt_interest_income: + 2027: 0 + output: + income_adjusted_part_d_premium_surcharge: 207.6 # $17.30 * 12 months + +- name: 2030 joint filers in the 35 percent tier, after the 20 percent reset. + period: 2030 + input: + age: 65 + filing_status: JOINT + adjusted_gross_income: + 2028: 270_000 + tax_exempt_interest_income: + 2028: 0 + output: + income_adjusted_part_d_premium_surcharge: 620.4 # $51.70 * 12 months diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_premium.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_premium.yaml index 98b889769ed..a2b4898d720 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_premium.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/eligibility/medicare_part_b_premium.yaml @@ -205,6 +205,70 @@ output: medicare_part_b_premium: 8_278.8 +# Projected years. The standard premium follows the Trustees' projection +# (Table V.E2), each income-related adjustment amount grows with it from +# CMS's 2026 amount, and the thresholds follow 42 U.S.C. 1395r(i)(5)'s CPI-U +# schedule: $112,000 / $141,000 / $176,000 / $211,000 / $500,000 unmarried in +# 2027, and twice that (but $888,000 at the top) jointly in 2034. +- name: 2027 single filer in the 50 percent tier. + period: 2027 + input: + age: 65 + filing_status: SINGLE + adjusted_gross_income: + 2025: 160_000 + tax_exempt_interest_income: + 2025: 0 + is_medicare_eligible: true + medicare_enrolled: true + msp_income_eligible: false + msp_asset_eligible: false + ssi: 0 # Not an SSI recipient, so no state buy-in. + output: + # ($209.50 standard + $209.50 adjustment) * 12 months + medicare_part_b_premium: 5_028 + +- name: 2027 separate filer above the falling top threshold. + period: 2027 + input: + age: 65 + filing_status: SEPARATE + adjusted_gross_income: + 2025: 390_000 + tax_exempt_interest_income: + 2025: 0 + is_medicare_eligible: true + medicare_enrolled: true + msp_income_eligible: false + msp_asset_eligible: false + ssi: 0 # Not an SSI recipient, so no state buy-in. + output: + # The top tier starts at $500,000 less the $112,000 unmarried threshold, + # $388,000 (CMS's 2026 figure was $391,000). The 85 percent tier's + # adjustment is $487.00 * 209.50 / 202.90 = $502.84, rounded to $502.80. + # ($209.50 + $502.80) * 12 months + medicare_part_b_premium: 8_547.6 + +- name: 2034 joint filers in the 65 percent tier. + period: 2034 + input: + age: 65 + filing_status: JOINT + adjusted_gross_income: + 2032: 450_000 + tax_exempt_interest_income: + 2032: 0 + is_medicare_eligible: true + medicare_enrolled: true + msp_income_eligible: false + msp_asset_eligible: false + ssi: 0 # Not an SSI recipient, so no state buy-in. + output: + # Joint 65 percent tier: $418,001 to $500,000. Adjustment $324.60 * + # 338.50 / 202.90 = $541.53, rounded to $541.50. + # ($338.50 + $541.50) * 12 months + medicare_part_b_premium: 10_560 + - name: A Part A-only enrollee pays no Part B premium. # $40,000 of interest is above the 135% FPL MSP limit, so with Part B # this person would pay the $185 standard premium: $2,220 a year. diff --git a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_part_b_premium_coverage.yaml b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_part_b_premium_coverage.yaml index 0c450945ee9..ef46dabc333 100644 --- a/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_part_b_premium_coverage.yaml +++ b/policyengine_us/tests/policy/baseline/gov/hhs/medicare/savings_programs/msp_part_b_premium_coverage.yaml @@ -27,3 +27,13 @@ base_part_b_premium: 2_220 output: msp_part_b_premium_coverage: 0 + +- name: MSP part B coverage pays the projected 2027 standard premium + period: 2027 + input: + is_medicare_eligible: true + medicare_enrolled: true + msp_income_eligible: true + msp_asset_eligible: true + output: + msp_part_b_premium_coverage: 2_514 # $209.50 * 12 months diff --git a/policyengine_us/tests/policy/baseline/gov/states/nh/doe/fap/nh_fap.yaml b/policyengine_us/tests/policy/baseline/gov/states/nh/doe/fap/nh_fap.yaml index abde032c84d..519a2d57478 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nh/doe/fap/nh_fap.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nh/doe/fap/nh_fap.yaml @@ -2809,8 +2809,8 @@ # Manual pp15,17: Social Security of 28,000 is 2,333.33 a month, or 2,313.33 after # the $20 exclusion, above the MSP limit (135% of the 2027 one-person guideline, -# about 1,838), so the enrollee pays 202.90 * 12 = 2,434.80. -# 28,000 - 2,434.80 = 25,565.20, 160.2% of 15,960: above 150% (23,940) and within +# about 1,838), so the enrollee pays the projected 2027 premium of 209.50 * 12 = 2,514. +# 28,000 - 2,514 = 25,486, 159.7% of 15,960: above 150% (23,940) and within # 175% (27,930), band E. The 2,000 oil bill is in tier BB; BB/E = 1,100. - name: "Case 86, the Part B deduction moves a Social Security recipient into a higher-paying band." period: 2027 @@ -2836,8 +2836,8 @@ members: ["person1"] state_code: "NH" output: - medicare_part_b_premium: 2_434.8 - nh_fap_countable_income: 25_565.2 + medicare_part_b_premium: 2_514 + nh_fap_countable_income: 25_486 nh_fap: 1_100 # Manual p15 and plan item 1.9: municipal general assistance is not counted. diff --git a/policyengine_us/tests/policy/baseline/gov/states/nh/doe/fap/nh_fap_countable_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/nh/doe/fap/nh_fap_countable_income.yaml index 90b0b480a34..e03d2242849 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nh/doe/fap/nh_fap_countable_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nh/doe/fap/nh_fap_countable_income.yaml @@ -819,7 +819,7 @@ # Railroad retirement counts as SSI unearned income for MSP, and 1,500 - 20 = # 1,480 a month is within the MSP limit (about 1,848 a month), so MSP status # turns on assets: 20,000 exceeds the 9,950 MSP resource limit and the enrollee -# pays the 2027 premium of 202.90 * 12 = 2,434.80. 18,000 - 2,434.80 = 15,565.20. +# pays the projected 2027 premium of 209.50 * 12 = 2,514. 18,000 - 2,514 = 15,486. - name: "Case 28, the Part B premium withheld from railroad retirement is deducted." period: 2027 absolute_error_margin: 0.01 @@ -842,12 +842,12 @@ members: ["person1"] state_code: "NH" output: - medicare_part_b_premium: 2_434.8 - nh_fap_countable_income: 15_565.2 + medicare_part_b_premium: 2_514 + nh_fap_countable_income: 15_486 # Manual pp14-17 allow support paid and the withheld Medicare premium separately. # Social Security of 2,000 a month is above the MSP limit (about 1,848 a month), -# so the premium is 2,434.80: 24,000 - 2,000 - 2,434.80 = 19,565.20. +# so the premium is 2,514: 24,000 - 2,000 - 2,514 = 19,486. - name: "Case 29, support paid and the Part B premium are both deducted." period: 2027 absolute_error_margin: 0.01 @@ -870,10 +870,10 @@ members: ["person1"] state_code: "NH" output: - medicare_part_b_premium: 2_434.8 - nh_fap_countable_income: 19_565.2 + medicare_part_b_premium: 2_514 + nh_fap_countable_income: 19_486 -# Manual pp14-17 deductions; 24,000 - 23,000 - 2,434.80 is negative and floors at 0. +# Manual pp14-17 deductions; 24,000 - 23,000 - 2,514 is negative and floors at 0. - name: "Case 30, combined allowed deductions cannot produce negative income." period: 2027 absolute_error_margin: 0.01 @@ -896,13 +896,13 @@ members: ["person1"] state_code: "NH" output: - medicare_part_b_premium: 2_434.8 + medicare_part_b_premium: 2_514 nh_fap_countable_income: 0 # Manual p17 and plan item 1.9 count Social Security net of the Medicare deduction. # An enrollee aged 70 needs no reported amount: 1,980 a month after the $20 # exclusion is above the MSP limit (about 1,848), so the premium is -# 202.90 * 12 = 2,434.80 and 24,000 - 2,434.80 = 21,565.20. +# the projected 2027 premium of 209.50 * 12 = 2,514, and 24,000 - 2,514 = 21,486. - name: "Case 31, a Medicare enrollee's Part B premium is deducted from Social Security." period: 2027 absolute_error_margin: 0.01 @@ -924,13 +924,13 @@ members: ["person1"] state_code: "NH" output: - medicare_part_b_premium: 2_434.8 - nh_fap_countable_income: 21_565.2 + medicare_part_b_premium: 2_514 + nh_fap_countable_income: 21_486 # Manual pp14-17; each member's withheld premium is deducted within its own SPM # unit. Separate tax and marital units keep each person's MSP test individual. # Person1 (Social Security above the MSP limit) and person2 (assets of 20,000 above -# the 9,950 limit) each pay 2,434.80: 24,000 + 18,000 - 2 * 2,434.80 = 37,130.40. +# the 9,950 limit) each pay 2,514: 24,000 + 18,000 - 2 * 2,514 = 36,972. # Person3's 980 a month after the $20 exclusion is within the MSP limit, so MSP # pays the premium and unit 2 keeps 12,000. - name: "Case 32, each SPM unit deducts its members' Part B premiums, and none that MSP pays." @@ -985,8 +985,8 @@ members: ["person1", "person2", "person3"] state_code: "NH" output: - medicare_part_b_premium: [2_434.8, 2_434.8, 0] - nh_fap_countable_income: [37_130.4, 12_000] + medicare_part_b_premium: [2_514, 2_514, 0] + nh_fap_countable_income: [36_972, 12_000] # Each SPM unit in the same household gets its own $50 exclusion. # Unit 1: 40 + 40 - 50 = 30. Unit 2: 25 + 25 - 50 = 0. @@ -1092,8 +1092,8 @@ # Manual p17 deducts Medicare premiums withheld from Social Security or railroad # retirement, so the deduction cannot exceed those benefits. Pension income of -# 30,000 keeps both enrollees above the MSP limit, so each pays 2,434.80. -# Unit 1: 30,000 + 1,200 - min(2,434.80, 1,200) = 30,000. +# 30,000 keeps both enrollees above the MSP limit, so each pays 2,514. +# Unit 1: 30,000 + 1,200 - min(2,514, 1,200) = 30,000. # Unit 2: no Social Security or railroad retirement, so 30,000 - 0 = 30,000. - name: "Case 36, the Part B deduction is capped at the Social Security and railroad benefits." period: 2027 @@ -1140,5 +1140,5 @@ members: ["person2"] state_code: "NH" output: - medicare_part_b_premium: [2_434.8, 2_434.8] + medicare_part_b_premium: [2_514, 2_514] nh_fap_countable_income: [30_000, 30_000] diff --git a/policyengine_us/tests/policy/baseline/gov/states/nh/doe/fap/nh_fap_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nh/doe/fap/nh_fap_eligible.yaml index 0a6b296b81d..4718c798a83 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nh/doe/fap/nh_fap_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nh/doe/fap/nh_fap_eligible.yaml @@ -915,8 +915,8 @@ output: nh_fap_eligible: true -# Manual pp17,74: the 2027 Part B premium of 202.90 * 12 = 2,434.80 withheld from -# Social Security is deducted for the enrollee: 50,000 - 2,434.80 = 47,565.20, within +# Manual pp17,74: the projected 2027 premium of 209.50 * 12 = 2,514 withheld from +# Social Security is deducted for the enrollee: 50,000 - 2,514 = 47,486, within # the 48,713 ceiling. Social Security of 50,000 is far above the MSP limit. The # person who is not enrolled in Medicare pays no premium and stays above it. - name: "Case 26, the withheld Part B premium brings an enrollee within the income ceiling." @@ -961,8 +961,8 @@ members: ["person2"] state_code: "NH" output: - medicare_part_b_premium: [0, 2_434.8] - nh_fap_countable_income: [50_000, 47_565.2] + medicare_part_b_premium: [0, 2_514] + nh_fap_countable_income: [50_000, 47_486] nh_fap_eligible: [false, true] # Manual pp12,18,21,35: estimate may replace an absent actual bill; retain income, qualified-member, subsidized heat-in-rent, fuel and state requirements. diff --git a/policyengine_us/tests/unit/test_ss_cola_default_invariance.py b/policyengine_us/tests/unit/test_ss_cola_default_invariance.py index 54284aef4ac..203ef051894 100644 --- a/policyengine_us/tests/unit/test_ss_cola_default_invariance.py +++ b/policyengine_us/tests/unit/test_ss_cola_default_invariance.py @@ -87,7 +87,7 @@ def irmaa_control(): values["medicare_irmaa_magi_two_years_prior"], [463100.31], rtol=0, atol=0.01 ) np.testing.assert_allclose( - values["medicare_part_b_premium"], [7790.40], rtol=0, atol=0.01 + values["medicare_part_b_premium"], [8043.60], rtol=0, atol=0.01 ) return situation, values