From 0fbe2eb84cc6f0489a29fc7910c3282c322c637f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 22:10:30 -0400 Subject: [PATCH 1/5] Bind the CMS Part B premium target to the enrollment-gated premium (#1206) The CMS premiums-from-enrollees target (2025 Trustees Report Table III.C3, CY2024 $139.837B) was mapped to gross_medicare_part_b_premium, which PolicyEngine-US defines for is_medicare_eligible, so it also charged eligible people who never enrolled. Map it to gross_medicare_part_b_premium_if_enrolled, defined for medicare_enrolled and documented in PolicyEngine-US for this target. Tests: the compiled spec binds the new variable; a differential test compiles the same fact under the old and new mapping and finds only base_variable differs; an engine-contract test pins both variables' gates in the locked policyengine-us. Co-Authored-By: Claude Opus 5.5 --- ...t-b-premium-target-enrollees-1206.fixed.md | 1 + .../build/us_runtime/fiscal_targets.py | 7 +- .../us/test_us_fiscal_targets.py | 26 +++++++ .../engine_free/us/test_us_fiscal_targets.py | 76 ++++++++++++++++--- 4 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 changelog.d/us-medicare-part-b-premium-target-enrollees-1206.fixed.md diff --git a/changelog.d/us-medicare-part-b-premium-target-enrollees-1206.fixed.md b/changelog.d/us-medicare-part-b-premium-target-enrollees-1206.fixed.md new file mode 100644 index 0000000000..71fd39839a --- /dev/null +++ b/changelog.d/us-medicare-part-b-premium-target-enrollees-1206.fixed.md @@ -0,0 +1 @@ +Bind the CMS Part B premiums-from-enrollees target to `gross_medicare_part_b_premium_if_enrolled`, the enrollment-gated gross premium, instead of `gross_medicare_part_b_premium`, which also charged eligible non-enrollees (#1206). diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/fiscal_targets.py b/packages/microcosm-build/src/microcosm/build/us_runtime/fiscal_targets.py index 2f0818f70e..33724b6594 100644 --- a/packages/microcosm-build/src/microcosm/build/us_runtime/fiscal_targets.py +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/fiscal_targets.py @@ -452,8 +452,13 @@ def _us_hierarchy_seed( ), }, ), + # 2025 Trustees Report Table III.C3 Part B premiums from enrollees. + # gross_medicare_part_b_premium is defined for is_medicare_eligible, so + # it also charges eligible people who never enrolled. PolicyEngine-US + # documents the enrollment-gated gross_medicare_part_b_premium_if_enrolled + # for CMS premiums-from-enrollees targets (microcosm#1206). ("cms_medicare", "actual_amount", "premiums_from_enrollees"): ( - "gross_medicare_part_b_premium", + "gross_medicare_part_b_premium_if_enrolled", "cms_medicare", {"target_role": "medicare_part_b_premium_total"}, ), diff --git a/packages/microcosm-build/tests/engine_contract/us/test_us_fiscal_targets.py b/packages/microcosm-build/tests/engine_contract/us/test_us_fiscal_targets.py index efa3e4dda8..bb2c739eee 100644 --- a/packages/microcosm-build/tests/engine_contract/us/test_us_fiscal_targets.py +++ b/packages/microcosm-build/tests/engine_contract/us/test_us_fiscal_targets.py @@ -55,3 +55,29 @@ def test_m_chip_state_set_matches_engine_separate_chip_child_limits() -> None: reviewed_carve_outs = {"44"} assert no_separate_child_chip == _M_CHIP_STATE_FIPS | reviewed_carve_outs assert not reviewed_carve_outs & _M_CHIP_STATE_FIPS + + +def test_medicare_part_b_premium_target_variable_is_enrollment_gated() -> None: + """The locked engine's premiums-from-enrollees variable (microcosm#1206). + + The target binds ``gross_medicare_part_b_premium_if_enrolled``. Pin that it + exists in the locked policyengine-us as a person-level annual amount gated + on ``medicare_enrolled``, and that the variable it replaced is gated only + on eligibility, so an engine bump that moves either gate re-opens the + mapping review. + """ + from policyengine_us import CountryTaxBenefitSystem + + base_variable, family, _ = fiscal_targets.DIRECT_LEDGER_TARGETS[ + ("cms_medicare", "actual_amount", "premiums_from_enrollees") + ] + assert family == "cms_medicare" + variables = CountryTaxBenefitSystem().variables + enrolled = variables[base_variable] + assert base_variable == "gross_medicare_part_b_premium_if_enrolled" + assert enrolled.entity.key == "person" + assert enrolled.definition_period == "year" + assert enrolled.defined_for == "medicare_enrolled" + assert variables["gross_medicare_part_b_premium"].defined_for == ( + "is_medicare_eligible" + ) diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_fiscal_targets.py b/packages/microcosm-build/tests/engine_free/us/test_us_fiscal_targets.py index de4f6c6827..94aa2c13ff 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_fiscal_targets.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_fiscal_targets.py @@ -1,6 +1,8 @@ """Tests split from packages/microcosm-build/tests/test_us_fiscal_targets.py.""" # ruff: noqa: F403, F405 +import dataclasses + from test_support.microcosm_build.us_fiscal_targets import * @@ -5140,16 +5142,22 @@ def test_soi_return_count_targets_expose_count_mode_without_base_variable() -> N assert "base_variables" not in spec.metadata -def test_medicare_part_b_premium_reference_uses_gross_premium_income() -> None: - source_record_id = ( - "cms_medicare.cy2024.part_b_premium_income." - "premiums_from_enrollees.actual_amount" - ) +_MEDICARE_PART_B_PREMIUM_RECORD_ID = ( + "cms_medicare.cy2024.part_b_premium_income.premiums_from_enrollees.actual_amount" +) +_MEDICARE_PART_B_PREMIUM_KEY = ( + "cms_medicare", + "actual_amount", + "premiums_from_enrollees", +) + + +def _compiled_medicare_part_b_premium_spec(): registry = compile_us_fiscal_target_registry( [ *packaged_reference_facts(), _dynamic_ledger_fact( - source_record_id=source_record_id, + source_record_id=_MEDICARE_PART_B_PREMIUM_RECORD_ID, source_name="cms_medicare", measure_id="actual_amount", groupby_value_id="premiums_from_enrollees", @@ -5158,13 +5166,63 @@ def test_medicare_part_b_premium_reference_uses_gross_premium_income() -> None: ], allow_unaged_dollar_targets=True, ) - specs = {spec.name: spec for spec in registry.specs} - spec = specs[source_record_id] + return specs[_MEDICARE_PART_B_PREMIUM_RECORD_ID] + + +def test_medicare_part_b_premium_reference_uses_enrolled_gross_premium() -> None: + """CMS premiums from enrollees bind the enrollment-gated gross premium. + + ``gross_medicare_part_b_premium`` is defined for ``is_medicare_eligible`` + and so also charges eligible non-enrollees (microcosm#1206). + """ + + spec = _compiled_medicare_part_b_premium_spec() assert spec.family == "cms_medicare" + assert spec.value == 139_837_000_000 assert spec.metadata["target_role"] == "medicare_part_b_premium_total" - assert spec.metadata["base_variable"] == "gross_medicare_part_b_premium" + assert spec.metadata["base_variable"] == ( + "gross_medicare_part_b_premium_if_enrolled" + ) assert spec.metadata["source_measure_id"] == "actual_amount" + assert spec.metadata["measure_mode"] == "sum" + assert "calibration_role" not in spec.metadata + + +def test_medicare_part_b_premium_remap_changes_only_the_bound_variable( + monkeypatch, +) -> None: + """Differential invariant for microcosm#1206's remap. + + Compiling the same ledger fact under the pre-#1206 mapping and the + current one yields the same target (name, value, period, entity, + family, sign, hierarchy and every metadata key) except the bound + variable. The value the calibration matches is the fact's value both + times. + """ + + current = _compiled_medicare_part_b_premium_spec() + base_variable, family, metadata = fiscal_targets.DIRECT_LEDGER_TARGETS[ + _MEDICARE_PART_B_PREMIUM_KEY + ] + assert base_variable == "gross_medicare_part_b_premium_if_enrolled" + monkeypatch.setitem( + fiscal_targets.DIRECT_LEDGER_TARGETS, + _MEDICARE_PART_B_PREMIUM_KEY, + ("gross_medicare_part_b_premium", family, metadata), + ) + previous = _compiled_medicare_part_b_premium_spec() + + assert previous.metadata["base_variable"] == "gross_medicare_part_b_premium" + current_metadata = dict(current.metadata) + previous_metadata = dict(previous.metadata) + assert current_metadata.pop("base_variable") != previous_metadata.pop( + "base_variable" + ) + assert current_metadata == previous_metadata + assert dataclasses.replace(current, metadata={}) == dataclasses.replace( + previous, metadata={} + ) def test_census_pep_population_age_facts_compile_to_count_targets() -> None: From 42d899c52f98c27821185aa9c54ae30446c7b91e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 22:30:17 -0400 Subject: [PATCH 2/5] Report held-out Medicare IRMAA payers and premiums (#1206) Adds us_medicare_irmaa_holdout.json beside the SSI holdout: weighted Part B and Part D income-related premium payers and annual amounts on the written release H5, compared with the 2026 Medicare Trustees Report, Tables V.E3 (p. 209) and V.E4 (p. 212), CY2024 historical rows: 4.8M payers and $12.8B (Part B), 4.2M and $2.2B (Part D). Validation only: it never enters the calibration registry and never refuses a release; an unavailable measurement is reported as such. Part B is gross_medicare_part_b_premium_if_enrolled minus base_part_b_premium for medicare_enrolled people, because PolicyEngine-US defines the base premium for every eligible person. Part D is income_adjusted_part_d_premium_surcharge. Invariants, tested (Hypothesis where stated): inputs are never written; totals scale with weights, are order invariant and additive over batches; payers are bounded by weighted enrollees; non-enrollees never reach Part B; the payload is batch-size invariant through the household-batched scorer; no ledger mapping or pinned Chronicle record set carries an IRMAA row. An engine-scenario test checks the measures on pe-us 2.2.1 and that its 2024 top-tier amounts equal the Trustees table. Co-Authored-By: Claude Opus 5.5 --- .../us-medicare-irmaa-holdout-1206.added.md | 1 + docs/agent-guide.md | 8 + .../us_runtime/medicare_irmaa_holdout.py | 338 +++++++++++++++++ .../us/test_us_fiscal_refresh_builder.py | 117 +++++- .../us/test_us_medicare_irmaa_holdout.py | 342 ++++++++++++++++++ .../us/test_us_post_export_scoring.py | 86 ++++- .../engine_free/us/test_us_spine_blindness.py | 2 + .../us/test_us_medicare_irmaa_holdout.py | 84 +++++ .../microcosm_build/us_post_export_scoring.py | 37 ++ tools/build_us_fiscal_refresh_release.py | 74 ++++ 10 files changed, 1086 insertions(+), 3 deletions(-) create mode 100644 changelog.d/us-medicare-irmaa-holdout-1206.added.md create mode 100644 packages/microcosm-build/src/microcosm/build/us_runtime/medicare_irmaa_holdout.py create mode 100644 packages/microcosm-build/tests/engine_free/us/test_us_medicare_irmaa_holdout.py create mode 100644 packages/microcosm-build/tests/engine_scenario/us/test_us_medicare_irmaa_holdout.py diff --git a/changelog.d/us-medicare-irmaa-holdout-1206.added.md b/changelog.d/us-medicare-irmaa-holdout-1206.added.md new file mode 100644 index 0000000000..2b041ac84c --- /dev/null +++ b/changelog.d/us-medicare-irmaa-holdout-1206.added.md @@ -0,0 +1 @@ +Report held-out Medicare Part B and Part D IRMAA payers and income-related premiums against the 2026 Trustees Report Tables V.E3 and V.E4 in `us_medicare_irmaa_holdout.json`; validation only, never a calibration target or release gate (#1206). diff --git a/docs/agent-guide.md b/docs/agent-guide.md index 964f502d48..f44ce33a4c 100644 --- a/docs/agent-guide.md +++ b/docs/agent-guide.md @@ -224,6 +224,14 @@ SSA's SSI Annual Statistical Report 2024, Table 9. This held-out diagnostic never feeds calibration or refuses a release; unavailable measurements are reported explicitly. The report records the difference between SSA's December 2024 recipient snapshot and the model's positive annual benefits. +It also writes `us_medicare_irmaa_holdout.json`: weighted Medicare Part B and +Part D income-related premium (IRMAA) payers and annual amounts, compared with +the 2026 Medicare Trustees Report's calendar-year rows in Tables V.E3 and V.E4 +(microcosm#1206). Part B counts +`gross_medicare_part_b_premium_if_enrolled - base_part_b_premium` for +`medicare_enrolled` people only, because the base premium is defined for every +eligible person. It is held out the same way: no calibration row, no release +refusal. Before a release rerun, run the small-H5 guard sweep described in [the release build rule](us-release-build-rule.md#post-export-scoring). That fixture check is separate from full-export timing and release diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/medicare_irmaa_holdout.py b/packages/microcosm-build/src/microcosm/build/us_runtime/medicare_irmaa_holdout.py new file mode 100644 index 0000000000..8974069114 --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/medicare_irmaa_holdout.py @@ -0,0 +1,338 @@ +"""Held-out Medicare IRMAA payers and premiums, never a calibration target. + +The US release reports this diagnostic beside the SSI holdout. The 2026 +Medicare Trustees Report's Part B and Part D income-related premium tables are +comparators for the model's payers and income-related premiums, not fitted +rows or a release gate (microcosm#1206). +""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from pathlib import Path +from types import MappingProxyType +from typing import Any + +import numpy as np + +__all__ = [ + "MEDICARE_IRMAA_HOLDOUT_VARIABLES", + "MEDICARE_IRMAA_TRUSTEES_2026", + "MedicareIrmaaBenchmark", + "TRUSTEES_REPORT_2026_PDF_SHA256", + "TRUSTEES_REPORT_2026_TITLE", + "TRUSTEES_REPORT_2026_URL", + "medicare_irmaa_holdout_from_sim", + "medicare_irmaa_holdout_payload", + "medicare_irmaa_holdout_unavailable", + "write_medicare_irmaa_holdout", +] + + +@dataclass(frozen=True) +class MedicareIrmaaBenchmark: + """One calendar-year row of a Trustees income-related premium table.""" + + part: str + label: str + table: str + printed_page: int + pdf_page: int + # "Beneficiaries affected (millions)" and "Aggregate premiums (billions)", + # published to one decimal place and stored here in persons and dollars. + beneficiaries_affected: int + aggregate_premiums: int + aggregate_premiums_definition: str + + @property + def average_monthly_premium_per_payer(self) -> float: + return self.aggregate_premiums / self.beneficiaries_affected / 12 + + +TRUSTEES_REPORT_2026_TITLE = ( + "2026 Annual Report of the Boards of Trustees of the Federal Hospital " + "Insurance and Federal Supplementary Medical Insurance Trust Funds" +) +TRUSTEES_REPORT_2026_URL = "https://www.cms.gov/oact/tr/2026" +#: SHA-256 of the report PDF the rows below were read from (271 pages). +TRUSTEES_REPORT_2026_PDF_SHA256 = ( + "ffa56b9137006872300b0346149eae1613d09a172b6ba118aad48e66dfc48fa8" +) +#: Rounding unit of the published counts (0.1 million) and dollars (0.1 billion). +_PUBLISHED_PAYER_UNIT = 100_000 +_PUBLISHED_PREMIUM_UNIT = 100_000_000 + +#: Tables V.E3 and V.E4, "Historical data", calendar year 2024. Fixed held-out +#: observations, never passed to the calibration target registry. +MEDICARE_IRMAA_TRUSTEES_2026 = MappingProxyType( + { + 2024: MappingProxyType( + { + "part_b": MedicareIrmaaBenchmark( + part="part_b", + label="Medicare Part B income-related premium", + table="V.E3", + printed_page=209, + pdf_page=215, + beneficiaries_affected=4_800_000, + aggregate_premiums=12_800_000_000, + aggregate_premiums_definition=( + "Represents the total amount paid by affected " + "beneficiaries in excess of the Part B standard premium." + ), + ), + "part_d": MedicareIrmaaBenchmark( + part="part_d", + label="Medicare Part D income-related premium", + table="V.E4", + printed_page=212, + pdf_page=218, + beneficiaries_affected=4_200_000, + aggregate_premiums=2_200_000_000, + aggregate_premiums_definition=( + "Represents the total amount paid by affected " + "beneficiaries in excess of the Part D plan premium." + ), + ), + } + ), + } +) + +_ENROLLED = "medicare_enrolled" +_PART_B_GROSS = "gross_medicare_part_b_premium_if_enrolled" +_PART_B_BASE = "base_part_b_premium" +_PART_D_SURCHARGE = "income_adjusted_part_d_premium_surcharge" +#: The person-level variables the release scorer computes, in request order. +MEDICARE_IRMAA_HOLDOUT_VARIABLES: tuple[str, ...] = ( + _ENROLLED, + _PART_B_GROSS, + _PART_B_BASE, + _PART_D_SURCHARGE, +) +_MODEL_MEASURES = MappingProxyType( + { + "part_b": ( + f"{_PART_B_GROSS} minus {_PART_B_BASE}, for persons with {_ENROLLED}" + ), + "part_d": _PART_D_SURCHARGE, + } +) + + +def medicare_irmaa_holdout_unavailable( + error: Exception, *, period: int, release_id: str | None = None +) -> dict[str, Any]: + """An observational receipt when plan discovery or scoring is unavailable.""" + payload: dict[str, Any] = { + "schema_version": 1, + "status": "unavailable", + "period": int(period), + "target_role": "validation", + "enforced": False, + "used_for_calibration": False, + "source_url": TRUSTEES_REPORT_2026_URL, + "error": f"{type(error).__name__}: {error}", + } + if release_id is not None: + payload["release_id"] = release_id + return payload + + +def _relative_difference(model: float, benchmark: float) -> float: + return model / benchmark - 1 + + +def _part_row( + part: str, + amounts: np.ndarray, + weights: np.ndarray, + benchmark: MedicareIrmaaBenchmark | None, +) -> dict[str, Any]: + payers = amounts > 0 + weighted_payers = float(weights[payers].sum()) + weighted_premiums = float((weights * amounts).sum()) + row: dict[str, Any] = { + "part": part, + "model_measure": _MODEL_MEASURES[part], + "weighted_payers": weighted_payers, + "weighted_aggregate_premiums": weighted_premiums, + "average_monthly_premium_per_payer": ( + weighted_premiums / weighted_payers / 12 if weighted_payers else None + ), + } + if benchmark is None: + row.update( + { + "benchmark_payers": None, + "benchmark_aggregate_premiums": None, + "benchmark_average_monthly_premium_per_payer": None, + "payers_relative_difference": None, + "aggregate_premiums_relative_difference": None, + } + ) + return row + row.update( + { + "label": benchmark.label, + "benchmark_table": benchmark.table, + "benchmark_printed_page": benchmark.printed_page, + "benchmark_pdf_page": benchmark.pdf_page, + "benchmark_aggregate_premiums_definition": ( + benchmark.aggregate_premiums_definition + ), + "benchmark_payers": benchmark.beneficiaries_affected, + "benchmark_aggregate_premiums": benchmark.aggregate_premiums, + "benchmark_average_monthly_premium_per_payer": ( + benchmark.average_monthly_premium_per_payer + ), + "payers_relative_difference": _relative_difference( + weighted_payers, benchmark.beneficiaries_affected + ), + "aggregate_premiums_relative_difference": _relative_difference( + weighted_premiums, benchmark.aggregate_premiums + ), + } + ) + return row + + +def medicare_irmaa_holdout_payload( + enrolled: np.ndarray, + part_b_gross_if_enrolled: np.ndarray, + part_b_base: np.ndarray, + part_d_surcharge: np.ndarray, + weights: np.ndarray, + *, + period: int, + release_id: str | None = None, +) -> dict[str, Any]: + """Weighted IRMAA payers and annual income-related premiums by Part. + + A person's Part B income-related premium is their enrollment-gated gross + Part B premium less the standard premium; ``base_part_b_premium`` is + defined for every Medicare-eligible person, so it is counted only for + ``medicare_enrolled`` people. The Part D amount is the engine's surcharge + as computed. A payer is a person with a positive annual amount. Each + person carries their household's calibrated weight, as supplied by the + written-H5 scorer's person results; the function reads its inputs and + never writes them. + """ + enrolled_values = np.asarray(enrolled) + part_b_gross, part_b_base, part_d_surcharge, weights = ( + np.asarray(values, dtype=np.float64) + for values in (part_b_gross_if_enrolled, part_b_base, part_d_surcharge, weights) + ) + if enrolled_values.ndim != 1 or any( + values.shape != enrolled_values.shape + for values in (part_b_gross, part_b_base, part_d_surcharge, weights) + ): + raise ValueError( + "Medicare IRMAA holdout needs aligned one-dimensional person arrays." + ) + if enrolled_values.dtype != np.bool_: + numeric = enrolled_values.astype(np.float64) + if not np.isin(numeric, (0.0, 1.0)).all(): + raise ValueError(f"{_ENROLLED} must be boolean.") + enrolled_values = numeric == 1.0 + if any( + not np.isfinite(values).all() + for values in (part_b_gross, part_b_base, part_d_surcharge, weights) + ) or np.any(weights < 0): + raise ValueError( + "Medicare IRMAA holdout needs finite values and nonnegative weights." + ) + if np.any(part_d_surcharge < 0): + raise ValueError(f"{_PART_D_SURCHARGE} must be nonnegative.") + part_b_amount = np.where(enrolled_values, part_b_gross - part_b_base, 0.0) + if np.any(part_b_amount < 0): + raise ValueError( + f"{_PART_B_GROSS} is below {_PART_B_BASE} for an enrolled person." + ) + + benchmarks = MEDICARE_IRMAA_TRUSTEES_2026.get(int(period)) + parts = [ + _part_row( + "part_b", + part_b_amount, + weights, + None if benchmarks is None else benchmarks["part_b"], + ), + _part_row( + "part_d", + part_d_surcharge, + weights, + None if benchmarks is None else benchmarks["part_d"], + ), + ] + payload: dict[str, Any] = { + "schema_version": 1, + "status": "available", + "period": int(period), + "target_role": "validation", + "enforced": False, + "used_for_calibration": False, + "measure": "Medicare income-related premium (IRMAA) payers and amounts", + "weight_basis": "person's calibrated household_weight", + "payer": "a person with a positive annual income-related premium", + "benchmark": { + "source": TRUSTEES_REPORT_2026_TITLE, + "source_url": TRUSTEES_REPORT_2026_URL, + "source_pdf_sha256": TRUSTEES_REPORT_2026_PDF_SHA256, + "tables": ["V.E3", "V.E4"], + "columns": [ + "Beneficiaries affected (millions)", + "Aggregate premiums (billions)", + ], + "year": None if benchmarks is None else int(period), + "basis": None if benchmarks is None else "Historical data", + "published_rounding": { + "payers": _PUBLISHED_PAYER_UNIT, + "aggregate_premiums": _PUBLISHED_PREMIUM_UNIT, + }, + "available_years": sorted(MEDICARE_IRMAA_TRUSTEES_2026), + }, + "comparability": ( + "The Trustees report the number of beneficiaries affected and the " + "aggregate additional premium amounts collected in a calendar " + "year; the tables do not state whether the count is an average " + "month, a year-end or an ever-in-year count. The model counts " + "persons with a " + "positive annual income-related premium in " + f"{int(period)} and sums those annual amounts. " + "This is a held-out diagnostic, not a calibration target or a " + "release gate." + ), + "parts": parts, + } + if release_id is not None: + payload["release_id"] = release_id + return payload + + +def medicare_irmaa_holdout_from_sim( + simulation: Any, *, period: int, release_id: str | None = None +) -> dict[str, Any]: + """Measure aligned person results from the household-batched H5 scorer.""" + results = { + variable: simulation.calculate(variable, period) + for variable in MEDICARE_IRMAA_HOLDOUT_VARIABLES + } + return medicare_irmaa_holdout_payload( + np.asarray(results[_ENROLLED]), + np.asarray(results[_PART_B_GROSS]), + np.asarray(results[_PART_B_BASE]), + np.asarray(results[_PART_D_SURCHARGE]), + np.asarray(results[_ENROLLED].weights), + period=period, + release_id=release_id, + ) + + +def write_medicare_irmaa_holdout(payload: dict[str, Any], path: Path | str) -> Path: + path = Path(path) + path.write_text( + json.dumps(payload, indent=2, sort_keys=True, allow_nan=False) + "\n" + ) + return path diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_fiscal_refresh_builder.py b/packages/microcosm-build/tests/engine_free/us/test_us_fiscal_refresh_builder.py index 3c62d5fd73..d26a88b48d 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_fiscal_refresh_builder.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_fiscal_refresh_builder.py @@ -6256,6 +6256,39 @@ def calculate(variable, period): def simulate(reform): return SimpleNamespace(calculate=calculate) + if name == "medicare_irmaa_holdout": + captured["irmaa_holdout_scored"] = self.dataset_path + record.update( + baseline_passes=1, + reform_passes=0, + reform_systems=0, + n_batches=1, + max_batch_households=2, + ) + + def calculate(variable, period): + assert period == builder.PERIOD + # One enrollee in IRMAA tier 1 (2024: $69.90 and $12.90 a + # month) and one eligible non-enrollee. + values = { + "medicare_enrolled": [True, False], + "gross_medicare_part_b_premium_if_enrolled": [ + 12 * (174.70 + 69.90), + 0.0, + ], + "base_part_b_premium": [12 * 174.70, 12 * 174.70], + "income_adjusted_part_d_premium_surcharge": [ + 12 * 12.90, + 0.0, + ], + } + return builder._PostExportValues( + np.array(values[variable]), np.array([2.0, 3.0]) + ) + + def simulate(reform): + return SimpleNamespace(calculate=calculate) + return SimpleNamespace(name=name, simulate=simulate, record=lambda: record) def finish_consumer(self, scoring): @@ -6348,6 +6381,18 @@ def fake_smoke(*, simulate, period): assert working_age["share"] == 0 assert older["weighted_ssi_recipients"] == 3 assert older["share"] == 1 + assert captured["irmaa_holdout_scored"] == captured["written_dataset"] + irmaa = json.loads((release_dir / "us_medicare_irmaa_holdout.json").read_text()) + assert irmaa["status"] == "available" + assert irmaa["release_id"] == release_id + assert irmaa["enforced"] is False + assert irmaa["used_for_calibration"] is False + assert irmaa["benchmark"]["year"] == 2024 + part_b, part_d = irmaa["parts"] + assert part_b["weighted_payers"] == 2 + assert part_b["weighted_aggregate_premiums"] == pytest.approx(2 * 12 * 69.90) + assert part_d["weighted_payers"] == 2 + assert part_d["weighted_aggregate_premiums"] == pytest.approx(2 * 12 * 12.90) build_manifest = json.loads((release_dir / "build_manifest.json").read_text()) release_manifest = json.loads((release_dir / "release_manifest.json").read_text()) if not skipped_smoke: @@ -6369,9 +6414,13 @@ def fake_smoke(*, simulate, period): assert block["post_export_scoring"] == { "dataset_sha256": build_manifest["dataset"]["sha256"], "consumers": ( - ["ssi_social_security_holdout"] + ["ssi_social_security_holdout", "medicare_irmaa_holdout"] if skipped_smoke - else ["reform_coverage_smoke", "ssi_social_security_holdout"] + else [ + "reform_coverage_smoke", + "ssi_social_security_holdout", + "medicare_irmaa_holdout", + ] ), } @@ -9008,6 +9057,7 @@ def fake_release_gate_failures(*args, **kwargs): "reform_validation", "demographics", "ssi_social_security_holdout", + "medicare_irmaa_holdout", ] assert captured["diagnostics"]["default_dataset"] == { "method": "l0_refit", @@ -12454,6 +12504,69 @@ def score_holdout(name, consumer, **kwargs): assert failures == [] +@pytest.mark.parametrize( + "scoring_available", (True, False), ids=("available", "unavailable") +) +def test_build_manifests_selects_medicare_irmaa_holdout_for_publication( + monkeypatch, tmp_path, scoring_available +) -> None: + from microcosm.build.us_runtime.medicare_irmaa_holdout import ( + medicare_irmaa_holdout_payload, + ) + from microcosm.data.contract import ( + _check_local_artifact_hashes, + _check_release_manifest, + ) + from microcosm.data.release import _release_manifest_release_artifacts + + builder = _load_builder_module() + release_id, release_dir, artifact_root = _gate_evidence_release_dir( + builder, monkeypatch, tmp_path, coverage={} + ) + + def score_holdout(name, consumer, **kwargs): + assert name == "medicare_irmaa_holdout" + if not scoring_available: + raise RuntimeError("holdout scoring is unavailable") + return medicare_irmaa_holdout_payload( + np.array([True, False]), + np.array([3_000.0, 0.0]), + np.array([2_000.0, 2_000.0]), + np.array([200.0, 0.0]), + np.array([1.0, 2.0]), + period=builder.PERIOD, + ) + + monkeypatch.setattr(builder, "_score_post_export_consumer", score_holdout) + builder._write_medicare_irmaa_holdout( + release_dir=release_dir, + dataset_path=artifact_root / builder.DATASET_FILENAME, + release_id=release_id, + ) + builder._build_manifests( + **_minimal_manifest_kwargs(builder, release_id, release_dir, artifact_root) + ) + + filename = "us_medicare_irmaa_holdout.json" + holdout_path = release_dir / filename + holdout = json.loads(holdout_path.read_text()) + assert holdout["status"] == ("available" if scoring_available else "unavailable") + assert holdout["release_id"] == release_id + manifest = json.loads((release_dir / "release_manifest.json").read_text()) + assert manifest["artifacts"]["us_medicare_irmaa_holdout"] == { + "kind": "diagnostics", + "path": filename, + "repo_id": builder.REPO_ID, + "revision": release_id, + "sha256": hashlib.sha256(holdout_path.read_bytes()).hexdigest(), + } + assert filename in _release_manifest_release_artifacts(release_dir) + failures: list[str] = [] + _check_release_manifest(manifest, release_id, failures) + _check_local_artifact_hashes(release_dir, manifest, failures) + assert failures == [] + + def test_build_manifests_binds_gate_evidence_and_the_qrf_tail_register( monkeypatch, tmp_path ) -> None: diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_medicare_irmaa_holdout.py b/packages/microcosm-build/tests/engine_free/us/test_us_medicare_irmaa_holdout.py new file mode 100644 index 0000000000..461f1c5f1e --- /dev/null +++ b/packages/microcosm-build/tests/engine_free/us/test_us_medicare_irmaa_holdout.py @@ -0,0 +1,342 @@ +"""Held-out Medicare IRMAA reporting without a country engine (microcosm#1206). + +Invariants: + +* The holdout never changes weights or any other input: it reads read-only + arrays and returns a new payload. +* Weighted payers and premiums are linear in the weights, invariant to person + order, and additive over any split of the persons (household batches). +* ``0 <= payers <= weighted enrollees`` for each Part; premiums are + nonnegative, and zero when no weight pays. +* Eligible non-enrollees never reach the Part B measure, whatever premium the + engine assigns them. +* The Trustees rows are validation only: no ledger mapping binds an IRMAA + measure, and the pinned Chronicle feed carries no income-related premium + record set. +""" + +import json +import math +from importlib.resources import files + +import numpy as np +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from microcosm.build.us_runtime import fiscal_targets +from microcosm.build.us_runtime.medicare_irmaa_holdout import ( + MEDICARE_IRMAA_HOLDOUT_VARIABLES, + MEDICARE_IRMAA_TRUSTEES_2026, + TRUSTEES_REPORT_2026_URL, + medicare_irmaa_holdout_payload, + medicare_irmaa_holdout_unavailable, + write_medicare_irmaa_holdout, +) + +# 2024 standard Part B premium and the tier 1 and tier 5 IRMAA amounts from +# Trustees Report Table V.E3 (monthly), and the Part D tier 1 amount (V.E4). +_BASE = 12 * 174.70 +_PART_B_TIER_1 = 12 * 69.90 +_PART_B_TIER_5 = 12 * 419.30 +_PART_D_TIER_1 = 12 * 12.90 + + +def _payload(enrolled, gross, base, part_d, weights, *, period=2024, **kwargs): + return medicare_irmaa_holdout_payload( + np.asarray(enrolled), + np.asarray(gross, dtype=np.float64), + np.asarray(base, dtype=np.float64), + np.asarray(part_d, dtype=np.float64), + np.asarray(weights, dtype=np.float64), + period=period, + **kwargs, + ) + + +def test_trustees_2026_rows_match_tables_v_e3_and_v_e4(): + """CY2024 historical rows: V.E3 p. 209 (PDF p. 215), V.E4 p. 212 (PDF p. 218).""" + rows = MEDICARE_IRMAA_TRUSTEES_2026[2024] + part_b, part_d = rows["part_b"], rows["part_d"] + assert (part_b.table, part_b.printed_page, part_b.pdf_page) == ("V.E3", 209, 215) + assert (part_d.table, part_d.printed_page, part_d.pdf_page) == ("V.E4", 212, 218) + assert part_b.beneficiaries_affected == 4_800_000 + assert part_b.aggregate_premiums == 12_800_000_000 + assert part_d.beneficiaries_affected == 4_200_000 + assert part_d.aggregate_premiums == 2_200_000_000 + assert part_b.average_monthly_premium_per_payer == pytest.approx(222.2222222) + assert part_d.average_monthly_premium_per_payer == pytest.approx(43.6507937) + assert "Part B standard premium" in part_b.aggregate_premiums_definition + assert "Part D plan premium" in part_d.aggregate_premiums_definition + assert sorted(MEDICARE_IRMAA_TRUSTEES_2026) == [2024] + with pytest.raises(TypeError): + MEDICARE_IRMAA_TRUSTEES_2026[2025] = rows + with pytest.raises(TypeError): + rows["part_c"] = part_b + + +def test_holdout_variables_are_the_engine_measures_in_request_order(): + assert MEDICARE_IRMAA_HOLDOUT_VARIABLES == ( + "medicare_enrolled", + "gross_medicare_part_b_premium_if_enrolled", + "base_part_b_premium", + "income_adjusted_part_d_premium_surcharge", + ) + + +def test_payers_and_premiums_count_only_enrollees_for_part_b(): + # Enrollee in tier 5; enrollee paying no IRMAA; eligible non-enrollee whose + # eligibility-gated base premium must not count; someone not eligible. + payload = _payload( + [True, True, False, False], + [_BASE + _PART_B_TIER_5, _BASE, 0.0, 0.0], + [_BASE, _BASE, _BASE, 0.0], + [_PART_D_TIER_1, 0.0, 0.0, 0.0], + [2.0, 3.0, 5.0, 7.0], + release_id="fixture", + ) + part_b, part_d = payload["parts"] + assert part_b["part"] == "part_b" + assert part_b["weighted_payers"] == 2 + assert part_b["weighted_aggregate_premiums"] == pytest.approx(2 * _PART_B_TIER_5) + assert part_b["average_monthly_premium_per_payer"] == pytest.approx(419.30) + assert part_b["benchmark_payers"] == 4_800_000 + assert part_b["payers_relative_difference"] == pytest.approx(2 / 4_800_000 - 1) + assert part_b["aggregate_premiums_relative_difference"] == pytest.approx( + 2 * _PART_B_TIER_5 / 12_800_000_000 - 1 + ) + assert part_b["benchmark_table"] == "V.E3" + assert part_d["part"] == "part_d" + assert part_d["weighted_payers"] == 2 + assert part_d["weighted_aggregate_premiums"] == pytest.approx(2 * _PART_D_TIER_1) + assert part_d["benchmark_table"] == "V.E4" + assert payload["release_id"] == "fixture" + assert payload["status"] == "available" + assert payload["target_role"] == "validation" + assert payload["enforced"] is False + assert payload["used_for_calibration"] is False + assert "gate" not in payload + assert payload["benchmark"]["source_url"] == TRUSTEES_REPORT_2026_URL + assert payload["benchmark"]["year"] == 2024 + assert payload["benchmark"]["tables"] == ["V.E3", "V.E4"] + assert "ever-in-year" in payload["comparability"] + + +def test_numeric_enrollment_flags_are_accepted_and_others_refused(): + flags = _payload( + [1, 0], [_BASE + _PART_B_TIER_1, 0.0], [_BASE, _BASE], [0, 0], [1, 1] + ) + booleans = _payload( + [True, False], [_BASE + _PART_B_TIER_1, 0.0], [_BASE, _BASE], [0, 0], [1, 1] + ) + assert flags == booleans + with pytest.raises(ValueError, match="boolean"): + _payload([2, 0], [0, 0], [0, 0], [0, 0], [1, 1]) + + +def test_a_period_without_a_trustees_row_reports_null_comparisons(tmp_path): + payload = _payload( + [True], [_BASE + _PART_B_TIER_1], [_BASE], [0.0], [4.0], period=2025 + ) + assert payload["benchmark"]["year"] is None + assert payload["benchmark"]["available_years"] == [2024] + for row in payload["parts"]: + assert row["benchmark_payers"] is None + assert row["benchmark_aggregate_premiums"] is None + assert row["payers_relative_difference"] is None + assert row["aggregate_premiums_relative_difference"] is None + part_b, part_d = payload["parts"] + assert part_b["weighted_payers"] == 4 + assert part_d["weighted_payers"] == 0 + assert part_d["average_monthly_premium_per_payer"] is None + path = write_medicare_irmaa_holdout(payload, tmp_path / "diagnostic.json") + assert json.loads(path.read_text()) == payload + + +def test_unavailable_payload_is_observational(): + payload = medicare_irmaa_holdout_unavailable( + KeyError("base_part_b_premium"), period=2024, release_id="fixture" + ) + assert payload["status"] == "unavailable" + assert payload["enforced"] is False + assert payload["used_for_calibration"] is False + assert payload["target_role"] == "validation" + assert payload["error"] == "KeyError: 'base_part_b_premium'" + assert payload["release_id"] == "fixture" + + +@pytest.mark.parametrize( + "arrays, message", + [ + (([True, False], [0.0], [0.0, 0.0], [0.0, 0.0], [1, 1]), "aligned"), + (([True], [math.nan], [0.0], [0.0], [1]), "finite"), + (([True], [0.0], [0.0], [0.0], [-1]), "nonnegative weights"), + (([True], [_BASE], [_BASE], [-1.0], [1]), "nonnegative"), + (([True], [_BASE - 1], [_BASE], [0.0], [1]), "below"), + ], +) +def test_malformed_inputs_are_refused_not_silently_reported(arrays, message): + with pytest.raises(ValueError, match=message): + _payload(*arrays) + + +# --------------------------------------------------------------------------- +# Property-based invariants +# --------------------------------------------------------------------------- + +_TIERS = (0.0, 69.90, 174.70, 279.50, 384.30, 419.30) +_PART_D_TIERS = (0.0, 12.90, 33.30, 53.80, 74.20, 81.00) + + +@st.composite +def _people(draw, min_size=0, max_size=40): + n = draw(st.integers(min_value=min_size, max_value=max_size)) + eligible = draw(st.lists(st.booleans(), min_size=n, max_size=n)) + takes_up = draw(st.lists(st.booleans(), min_size=n, max_size=n)) + tiers = draw(st.lists(st.integers(0, 5), min_size=n, max_size=n)) + weights = draw( + st.lists( + st.floats(0, 1e4, allow_nan=False, allow_infinity=False), + min_size=n, + max_size=n, + ) + ) + enrolled = np.asarray(eligible, dtype=bool) & np.asarray(takes_up, dtype=bool) + tier = np.asarray(tiers, dtype=np.int64) + base = np.where(np.asarray(eligible, dtype=bool), _BASE, 0.0) + irmaa = 12 * np.asarray(_TIERS)[tier] + gross = np.where(enrolled, base + irmaa, 0.0) + part_d = np.where(enrolled, 12 * np.asarray(_PART_D_TIERS)[tier], 0.0) + return enrolled, gross, base, part_d, np.asarray(weights, dtype=np.float64) + + +def _totals(payload): + return [ + (row["weighted_payers"], row["weighted_aggregate_premiums"]) + for row in payload["parts"] + ] + + +@settings(max_examples=200, deadline=None) +@given(_people()) +def test_holdout_never_writes_its_inputs(people): + arrays = [np.array(values, copy=True) for values in people] + for values in arrays: + values.flags.writeable = False + payload = _payload(*arrays) + for before, after in zip(people, arrays, strict=True): + np.testing.assert_array_equal(before, after) + json.dumps(payload, allow_nan=False) + + +@settings(max_examples=200, deadline=None) +@given(_people(), st.floats(0.5, 4.0, allow_nan=False)) +def test_totals_scale_with_the_weights(people, scale): + enrolled, gross, base, part_d, weights = people + reference = _totals(_payload(enrolled, gross, base, part_d, weights)) + scaled = _totals(_payload(enrolled, gross, base, part_d, weights * scale)) + for (payers, premiums), (scaled_payers, scaled_premiums) in zip( + reference, scaled, strict=True + ): + assert scaled_payers == pytest.approx(payers * scale, rel=1e-9, abs=1e-6) + assert scaled_premiums == pytest.approx(premiums * scale, rel=1e-9, abs=1e-3) + + +@settings(max_examples=200, deadline=None) +@given(_people(min_size=1), st.randoms(use_true_random=False), st.data()) +def test_totals_are_order_invariant_and_additive_over_batches(people, rng, data): + enrolled, gross, base, part_d, weights = people + n = len(weights) + reference = _totals(_payload(*people)) + order = list(range(n)) + rng.shuffle(order) + shuffled = _totals(_payload(*(values[order] for values in people))) + split = data.draw(st.integers(0, n)) + first = _totals(_payload(*(values[:split] for values in people))) + second = _totals(_payload(*(values[split:] for values in people))) + for whole, permuted, left, right in zip( + reference, shuffled, first, second, strict=True + ): + for index in range(2): + assert permuted[index] == pytest.approx(whole[index], rel=1e-9, abs=1e-3) + assert left[index] + right[index] == pytest.approx( + whole[index], rel=1e-9, abs=1e-3 + ) + + +@settings(max_examples=200, deadline=None) +@given(_people()) +def test_payers_and_premiums_are_bounded_by_enrollment(people): + enrolled, gross, base, part_d, weights = people + payload = _payload(*people) + weighted_enrollees = float(weights[enrolled].sum()) + for row in payload["parts"]: + assert 0 <= row["weighted_payers"] <= weighted_enrollees + 1e-6 + assert row["weighted_aggregate_premiums"] >= 0 + if row["weighted_payers"] == 0: + assert row["weighted_aggregate_premiums"] == 0 + assert row["average_monthly_premium_per_payer"] is None + + +@settings(max_examples=200, deadline=None) +@given(_people(min_size=1), st.data()) +def test_non_enrollees_never_reach_the_part_b_measure(people, data): + """Whatever base premium a non-enrollee carries leaves Part B unchanged.""" + enrolled, gross, base, part_d, weights = people + reference = _payload(*people)["parts"][0] + noise = np.asarray( + data.draw( + st.lists( + st.floats(0, 1e5, allow_nan=False), + min_size=len(base), + max_size=len(base), + ) + ) + ) + perturbed_base = np.where(enrolled, base, noise) + perturbed = _payload(enrolled, gross, perturbed_base, part_d, weights)["parts"][0] + assert perturbed == reference + + +# --------------------------------------------------------------------------- +# Validation only: the Trustees IRMAA rows cannot reach calibration +# --------------------------------------------------------------------------- + + +def test_no_ledger_mapping_binds_an_irmaa_measure(): + irmaa_only = {"base_part_b_premium", "income_adjusted_part_d_premium_surcharge"} + mapped = [] + for mapping in ( + fiscal_targets.DIRECT_LEDGER_TARGETS, + fiscal_targets.INDICATOR_LEDGER_TARGETS, + ): + for key, (base_variable, _, metadata) in mapping.items(): + variables = ( + (base_variable,) if isinstance(base_variable, str) else base_variable + ) + mapped.append((key, set(variables), dict(metadata))) + for key, variables, metadata in mapped: + assert not variables & irmaa_only, key + assert "irmaa" not in json.dumps([key, metadata]).lower(), key + assert "income_related" not in json.dumps([key, metadata]).lower(), key + + +def test_pinned_chronicle_feed_carries_no_income_related_premium_rows(): + """Only a feed re-pin could bring a Trustees IRMAA row to calibration.""" + scope = json.loads( + files("microcosm.build.us").joinpath("chronicle_feed_scope.json").read_text() + ) + medicare = { + (pair["package_id"], pair["record_set_id"]) + for pair in scope["pairs"] + if pair["record_set_id"].startswith("cms_medicare") + } + assert medicare == { + ( + "cms-medicare-trustees-report-2025-part-b-premium-income", + "cms_medicare.cy2024.part_b_premium_income", + ) + } + for pair in scope["pairs"]: + text = json.dumps(pair).lower() + assert "irmaa" not in text and "income_related" not in text, pair diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_post_export_scoring.py b/packages/microcosm-build/tests/engine_free/us/test_us_post_export_scoring.py index 69bc7e36f1..fd4477bb4d 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_post_export_scoring.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_post_export_scoring.py @@ -464,6 +464,7 @@ def default_tax_benefit_system(*args, **kwargs): "reform_validation", "demographics", "ssi_social_security_holdout", + "medicare_irmaa_holdout", ] smoke_keys = { (probe.budget_measure, int(probe.period or builder.PERIOD), None) @@ -477,6 +478,12 @@ def default_tax_benefit_system(*args, **kwargs): ("ssi", builder.PERIOD, None), ("social_security", builder.PERIOD, None), ) + assert plan.baseline_plan("medicare_irmaa_holdout") == ( + ("medicare_enrolled", builder.PERIOD, None), + ("gross_medicare_part_b_premium_if_enrolled", builder.PERIOD, None), + ("base_part_b_premium", builder.PERIOD, None), + ("income_adjusted_part_d_premium_surcharge", builder.PERIOD, None), + ) validation = plan.baseline_plan("reform_validation") assert ("income_tax", builder.PERIOD, None) in validation assert ("state_code_str", builder.PERIOD, None) in validation @@ -495,7 +502,82 @@ def default_tax_benefit_system(*args, **kwargs): result=_empty_calibration_result(), release_id="fixture", ) - assert list(skipped) == ["demographics", "ssi_social_security_holdout"] + assert list(skipped) == [ + "demographics", + "ssi_social_security_holdout", + "medicare_irmaa_holdout", + ] + + +def test_medicare_irmaa_holdout_is_invariant_to_household_batch_size( + builder, tmp_path +) -> None: + frame = _nested_frame() + consumer = builder._medicare_irmaa_holdout_consumer + reference = consumer(_unbatched_simulate(frame, _EngineLog())) + assert reference["status"] == "available" + part_b, part_d = reference["parts"] + # The fixture's payers sit in several households, and two eligible + # non-enrollees carry a base premium the Part B measure must not count. + assert part_b["weighted_payers"] > 0 + assert part_d["weighted_payers"] > 0 + for batch_size in (1, 2, None): + log = _EngineLog() + scorer = _scorer(builder, frame, log, batch_size, tmp_path / str(batch_size)) + plan = builder._record_post_export_baseline_plan(consumer) + scoring = scorer.open_consumer("medicare_irmaa_holdout", plan) + payload = consumer(scoring.simulate) + assert payload == reference + record = scorer.finish_consumer(scoring) + assert record["baseline_passes"] == 1 + assert record["reform_passes"] == record["reform_systems"] == 0 + assert len(log.constructions) == scorer.n_batches + scorer.close() + + +def test_medicare_irmaa_holdout_measurement_error_is_report_only( + builder, monkeypatch, tmp_path +) -> None: + def unavailable(*args, **kwargs): + raise ValueError("fixture unavailable") + + monkeypatch.setattr(builder, "_score_post_export_consumer", unavailable) + builder._write_medicare_irmaa_holdout( + release_dir=tmp_path, + dataset_path=tmp_path / "fixture.h5", + release_id="fixture", + ) + payload = json.loads((tmp_path / "us_medicare_irmaa_holdout.json").read_text()) + assert payload["status"] == "unavailable" + assert payload["error"] == "ValueError: fixture unavailable" + assert payload["enforced"] is False + assert payload["used_for_calibration"] is False + + +def test_medicare_irmaa_holdout_discovery_error_does_not_fail_the_plan( + builder, monkeypatch +) -> None: + def unavailable(*args, **kwargs): + raise KeyError("income_adjusted_part_d_premium_surcharge") + + monkeypatch.setattr(builder, "medicare_irmaa_holdout_from_sim", unavailable) + args = SimpleNamespace( + skip_reform_coverage_smoke=True, + skip_reform_validation=True, + skip_out_of_sample_reforms=True, + skip_demographics=True, + maximum_microsim_batch_size=3, + ) + plan = builder._record_post_export_scoring_plan( + args, + n_households=7, + result=_empty_calibration_result(), + release_id="fixture", + ) + assert plan.error is None + assert plan.terminal_failures() == [] + assert plan.baseline_plan("medicare_irmaa_holdout") == () + assert builder._medicare_irmaa_holdout_consumer(None)["status"] == "unavailable" def test_ssi_social_security_holdout_is_invariant_to_household_batch_size( @@ -656,6 +738,7 @@ def test_an_unbuildable_plan_is_recorded_and_refused_not_raised( "reform_validation", "demographics", "ssi_social_security_holdout", + "medicare_irmaa_holdout", ] empty = builder._PostExportScoringPlan( n_households=7, maximum_batch_size=3, baseline_plans={} @@ -733,6 +816,7 @@ def test_main_and_writers_never_build_a_whole_pool_simulation(builder) -> None: "_write_reform_validation", "_write_demographics", "_write_ssi_social_security_holdout", + "_write_medicare_irmaa_holdout", ): _, tree = _function_source(builder, name) called = _called_names(tree) diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_spine_blindness.py b/packages/microcosm-build/tests/engine_free/us/test_us_spine_blindness.py index 8aab193a3d..284149bdf1 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_spine_blindness.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_spine_blindness.py @@ -276,6 +276,8 @@ "l0_refit_export.py", "late_producer_dag.py", # Pure contract/schedule derivation; no treatment. "medicaid_take_up.py", + # Reviewed held-out Medicare IRMAA diagnostic; no population treatment. + "medicare_irmaa_holdout.py", "misc_itemized.py", "nonzero_shares.py", "operator_boundary.py", # Raw-stage validator; no population treatment. diff --git a/packages/microcosm-build/tests/engine_scenario/us/test_us_medicare_irmaa_holdout.py b/packages/microcosm-build/tests/engine_scenario/us/test_us_medicare_irmaa_holdout.py new file mode 100644 index 0000000000..18badfacb9 --- /dev/null +++ b/packages/microcosm-build/tests/engine_scenario/us/test_us_medicare_irmaa_holdout.py @@ -0,0 +1,84 @@ +"""The IRMAA holdout's measures on the locked policyengine-us (microcosm#1206).""" + +import numpy as np +import pytest + +from microcosm.build.us_runtime.medicare_irmaa_holdout import ( + MEDICARE_IRMAA_HOLDOUT_VARIABLES, + medicare_irmaa_holdout_payload, +) + +# 2026 Trustees Report Tables V.E3 (p. 209) and V.E4 (p. 212), 2024 row, +# 85 percent column: the top monthly income-related adjustment amounts. +_TRUSTEES_2024_PART_B_TOP_MONTHLY = 419.30 +_TRUSTEES_2024_PART_D_TOP_MONTHLY = 81.00 + + +def _person(take_up: bool) -> dict: + return {"age": {"2024": 70}, "takes_up_medicare_if_eligible": {"2024": take_up}} + + +def test_holdout_measures_follow_the_engine_on_a_household() -> None: + """An enrollee and an eligible non-enrollee, each a single filer whose + IRMAA MAGI ($600,000) is given for 2024, so it sits in the top tier. + + The engine charges the enrollee the Trustees' 2024 top-tier amounts, and + gives the non-enrollee the eligibility-gated base premium but no + enrollment-gated gross premium: the holdout counts only the enrollee. + """ + from policyengine_us import Simulation + + situation = { + "people": {"enrollee": _person(True), "non_enrollee": _person(False)}, + "tax_units": { + "enrollee_unit": { + "members": ["enrollee"], + "medicare_irmaa_magi_two_years_prior": {"2024": 600_000}, + }, + "non_enrollee_unit": { + "members": ["non_enrollee"], + "medicare_irmaa_magi_two_years_prior": {"2024": 600_000}, + }, + }, + "families": {"family": {"members": ["enrollee", "non_enrollee"]}}, + "spm_units": {"spm_unit": {"members": ["enrollee", "non_enrollee"]}}, + "marital_units": { + "enrollee_marital_unit": {"members": ["enrollee"]}, + "non_enrollee_marital_unit": {"members": ["non_enrollee"]}, + }, + "households": { + "household": { + "members": ["enrollee", "non_enrollee"], + "state_code": {"2024": "TX"}, + } + }, + } + simulation = Simulation(situation=situation) + values = { + variable: np.asarray(simulation.calculate(variable, 2024)) + for variable in MEDICARE_IRMAA_HOLDOUT_VARIABLES + } + enrolled = values["medicare_enrolled"] + gross = values["gross_medicare_part_b_premium_if_enrolled"] + base = values["base_part_b_premium"] + part_d = values["income_adjusted_part_d_premium_surcharge"] + + np.testing.assert_array_equal(enrolled, [True, False]) + assert base[1] == base[0] > 0 + assert gross[1] == 0 + assert gross[0] - base[0] == pytest.approx(12 * _TRUSTEES_2024_PART_B_TOP_MONTHLY) + assert part_d[0] == pytest.approx(12 * _TRUSTEES_2024_PART_D_TOP_MONTHLY) + assert part_d[1] == 0 + + payload = medicare_irmaa_holdout_payload( + enrolled, gross, base, part_d, np.array([1.0, 1.0]), period=2024 + ) + part_b_row, part_d_row = payload["parts"] + assert part_b_row["weighted_payers"] == 1 + assert part_b_row["average_monthly_premium_per_payer"] == pytest.approx( + _TRUSTEES_2024_PART_B_TOP_MONTHLY + ) + assert part_d_row["weighted_payers"] == 1 + assert part_d_row["average_monthly_premium_per_payer"] == pytest.approx( + _TRUSTEES_2024_PART_D_TOP_MONTHLY + ) diff --git a/test_support/microcosm_build/us_post_export_scoring.py b/test_support/microcosm_build/us_post_export_scoring.py index 875bf13e73..ca8704a815 100644 --- a/test_support/microcosm_build/us_post_export_scoring.py +++ b/test_support/microcosm_build/us_post_export_scoring.py @@ -221,6 +221,10 @@ def sum(self) -> float: "medicare_cost", "head_start", "wic", + "medicare_enrolled", + "base_part_b_premium", + "gross_medicare_part_b_premium_if_enrolled", + "income_adjusted_part_d_premium_surcharge", } ) _SPM_UNIT_VARIABLES = frozenset({"snap", "housing_assistance", "spm_unit_benefits"}) @@ -257,6 +261,37 @@ def _household_positions(frame: Frame, entity: str) -> np.ndarray: return household_positions.reindex(unit_households.to_numpy()).to_numpy() +_MEDICARE_HOLDOUT_FIXTURE_VARIABLES = frozenset( + { + "medicare_enrolled", + "base_part_b_premium", + "gross_medicare_part_b_premium_if_enrolled", + "income_adjusted_part_d_premium_surcharge", + } +) + + +def _medicare_holdout_fixture_values(person: pd.DataFrame, variable: str): + """Fixture Medicare semantics spread over several households. + + Not the law: people aged 30 or older are "eligible" so that payers sit + in several households. Eligible people with ``person_id % 5 == 0`` do not + enroll, yet carry the eligibility-gated base premium, as in + policyengine-us. Integer dollars keep batched sums exact. + """ + person_id = person["person_id"].to_numpy() + eligible = person["age"].to_numpy() >= 30 + enrolled = eligible & (person_id % 5 != 0) + tier = person_id % 3 + if variable == "medicare_enrolled": + return enrolled + if variable == "base_part_b_premium": + return np.where(eligible, 2_096.0, 0.0) + if variable == "gross_medicare_part_b_premium_if_enrolled": + return np.where(enrolled, 2_096.0 + 840.0 * tier, 0.0) + return np.where(enrolled, 156.0 * tier, 0.0) + + def _native_values(frame: Frame, variable: str, period: int, delta: float): person = frame.table("person") state_fips = frame.table("household")["state_fips"].to_numpy() @@ -273,6 +308,8 @@ def _native_values(frame: Frame, variable: str, period: int, delta: float): return (person["age"].to_numpy() < 18).astype(np.float64) if variable == "in_poverty": return person["person_id"].to_numpy() % 3 == 0 + if variable in _MEDICARE_HOLDOUT_FIXTURE_VARIABLES: + return _medicare_holdout_fixture_values(person, variable) entity = _native_entity(variable) ids = frame.table(entity)[f"{entity}_id"].to_numpy() base = zlib.crc32(f"{variable}@{period}".encode()) % 89 diff --git a/tools/build_us_fiscal_refresh_release.py b/tools/build_us_fiscal_refresh_release.py index f1310ed554..6b64ce7059 100644 --- a/tools/build_us_fiscal_refresh_release.py +++ b/tools/build_us_fiscal_refresh_release.py @@ -263,6 +263,11 @@ attach_l0_refit_entity_weights, load_us_frame, ) +from microcosm.build.us_runtime.medicare_irmaa_holdout import ( + medicare_irmaa_holdout_from_sim, + medicare_irmaa_holdout_unavailable, + write_medicare_irmaa_holdout, +) from microcosm.build.us_runtime.nonzero_shares import us_nonzero_shares from microcosm.build.us_runtime.parity_reference import ( EcpsParityReference, @@ -4927,6 +4932,15 @@ def _ssi_social_security_holdout_consumer(simulate) -> dict[str, Any]: return ssi_social_security_holdout_unavailable(error, period=PERIOD) +def _medicare_irmaa_holdout_consumer(simulate) -> dict[str, Any]: + try: + return medicare_irmaa_holdout_from_sim(simulate(None), period=PERIOD) + except Exception as error: + # As for the SSI holdout: an optional diagnostic adds no failure to + # plan discovery when its inputs are unavailable (microcosm#1206). + return medicare_irmaa_holdout_unavailable(error, period=PERIOD) + + def _reform_validation_consumer( *, result, release_id: str ) -> Callable[[Callable[[Any], Any] | None], dict[str, Any]]: @@ -4964,6 +4978,7 @@ def _post_export_consumers( if not args.skip_demographics: consumers["demographics"] = _demographics_consumer consumers["ssi_social_security_holdout"] = _ssi_social_security_holdout_consumer + consumers["medicare_irmaa_holdout"] = _medicare_irmaa_holdout_consumer return consumers @@ -10285,6 +10300,37 @@ def _write_ssi_social_security_holdout( ) +def _write_medicare_irmaa_holdout( + *, + release_dir: Path, + dataset_path: Path, + release_id: str, + post_export_scorer: _HouseholdBatchedPostExportScorer | None = None, + baseline_plan: Sequence[PostExportKey] | None = None, + maximum_microsim_batch_size: int | None = DEFAULT_MAXIMUM_MICROSIM_BATCH_SIZE, +) -> None: + """Report held-out IRMAA payers and premiums; misses never refuse a release.""" + try: + payload = _score_post_export_consumer( + "medicare_irmaa_holdout", + _medicare_irmaa_holdout_consumer, + dataset_path=dataset_path, + post_export_scorer=post_export_scorer, + baseline_plan=baseline_plan, + maximum_microsim_batch_size=maximum_microsim_batch_size, + ) + payload["release_id"] = release_id + except Exception as error: + # Reporting is observational, including an unavailable measurement. + # It must not introduce a new release refusal or calibration target. + payload = medicare_irmaa_holdout_unavailable( + error, period=PERIOD, release_id=release_id + ) + write_medicare_irmaa_holdout( + payload, release_dir / "us_medicare_irmaa_holdout.json" + ) + + def _build_manifests( *, release_id: str, @@ -10879,6 +10925,18 @@ def _build_manifests( if (release_dir / "us_ssi_social_security_holdout.json").exists() else {} ), + **( + { + "us_medicare_irmaa_holdout": _artifact_entry( + "us_medicare_irmaa_holdout.json", + _sha256(release_dir / "us_medicare_irmaa_holdout.json"), + kind="diagnostics", + revision=release_id, + ) + } + if (release_dir / "us_medicare_irmaa_holdout.json").exists() + else {} + ), **( { "reform_validation": _artifact_entry( @@ -15741,6 +15799,22 @@ def _main(argv: Sequence[str] | None = None) -> int | None: "us_ssi_social_security_holdout", release_dir / "us_ssi_social_security_holdout.json", ) + telemetry.stage( + "medicare_irmaa_holdout", + message="Writing held-out Medicare IRMAA payers and premiums.", + ) + _write_medicare_irmaa_holdout( + release_dir=release_dir, + dataset_path=dataset_path, + release_id=release_id, + post_export_scorer=post_export_scorer, + baseline_plan=post_export_scoring_plan.baseline_plan("medicare_irmaa_holdout"), + ) + if telemetry is not None: + telemetry.attach_artifact( + "us_medicare_irmaa_holdout", + release_dir / "us_medicare_irmaa_holdout.json", + ) # Every post-export engine stage has run; free the scorer's batch frames. # The manifest must pin the bytes those stages scored, and both manifests # carry how they were scored (Route A remediation PR-3). From a3accc948608e9e5edcc38f07fbd18751dbefb7e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 22:38:57 -0400 Subject: [PATCH 3/5] Re-pin the national_state surface version for the Part B remap (#1206) The registry version hashes target metadata, so binding the Part B premium target to gross_medicare_part_b_premium_if_enrolled moves it from f96be9f41781 to e1f9906bf0e0. Target counts are unchanged, and the differential test pins that only base_variable differs. National targets' concept-budget keys exclude base_variable, so the target's loss weight is unchanged too. Co-Authored-By: Claude Opus 5.5 --- .../tests/engine_free/us/test_us_fiscal_targets.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_fiscal_targets.py b/packages/microcosm-build/tests/engine_free/us/test_us_fiscal_targets.py index 94aa2c13ff..3cd976bac3 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_fiscal_targets.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_fiscal_targets.py @@ -895,8 +895,11 @@ def test_pinned_feed_national_state_surface_restores_the_fences( ) -> None: """The release surface on the pinned feed: 33,250 compiled targets after Medicaid substitution, 6,102 national_state targets at registry - f96be9f41781, 32 CHIP rows none of them for an M-CHIP state, no - other-income row and no tips return count. The 408 state AGI-band rows of + e1f9906bf0e0, 32 CHIP rows none of them for an M-CHIP state, no + other-income row and no tips return count. microcosm#1206 moved the + version from f96be9f41781 by binding the Part B premium target to + gross_medicare_part_b_premium_if_enrolled; the counts are unchanged. + The 408 state AGI-band rows of microcosm#940 (51 states x 4 bands x 2 measures) took it from 32,842 / 5,694 at d315c75804ef on the c5e5bf8 feed; before microcosm#956 it was 32,867 / 5,719 at d5f9d854fe11, and before decision d179 dropped the @@ -905,7 +908,7 @@ def test_pinned_feed_national_state_surface_restores_the_fences( registry, surface, _ = pinned_feed_national_state_surface assert len(registry.specs) == 33_250 assert len(surface.specs) == 6_102 - assert surface.version == "f96be9f41781" + assert surface.version == "e1f9906bf0e0" chip = [ spec for spec in surface.specs From 5570ee8b7eea77fe3c31e9dc0a21f20ec91d32d4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 22:58:20 -0400 Subject: [PATCH 4/5] Address review: withhold IRMAA comparisons with no payers; state the 2.2.1 caveat (#1206) Independent review (GPT-6.1 Sol, ~/reviews/irmaa-1206/review_42d899c52.md): - B1: on the locked policyengine-us 2.2.1 a 2024 release has no IRMAA (it rests on income two years earlier, absent from a single-year H5), so the holdout would have reported "available" with -100% comparisons. It now reports status no_payers (or no_medicare_enrollees) with a reason and withholds the Trustees comparisons. An engine-scenario test pins the locked engine's zero for a 2024-only household, so a bump that includes pe-us#10106 changes it visibly. - B2: the target comment and changelog say the bound premium carries no 2024 IRMAA until Microcosm's engine includes pe-us#10106. - N1: the payload and agent guide name both enrollment gates (Part A and/or B; Part D surcharge on every enrollee before pe-us#10115). - N2: Part D now counts medicare_enrolled people only, like Part B, and reports any surcharge outside enrollment; the Hypothesis property perturbs non-enrollees' Part B and Part D values. - N3: the feed test keeps only the IRMAA text check; a new test pins that only the release tool imports the holdout. - N4: the historical compiled-filter fixture's docstring notes it predates the remap. Co-Authored-By: Claude Opus 5.5 --- .../us-medicare-irmaa-holdout-1206.added.md | 2 +- ...t-b-premium-target-enrollees-1206.fixed.md | 2 +- docs/agent-guide.md | 17 +- .../build/us_runtime/fiscal_targets.py | 5 + .../us_runtime/medicare_irmaa_holdout.py | 106 +++++++++---- .../us/test_us_fiscal_refresh_builder.py | 5 +- .../us/test_us_medicare_irmaa_holdout.py | 148 ++++++++++++++---- .../us/test_us_medicare_irmaa_holdout.py | 38 +++++ 8 files changed, 258 insertions(+), 65 deletions(-) diff --git a/changelog.d/us-medicare-irmaa-holdout-1206.added.md b/changelog.d/us-medicare-irmaa-holdout-1206.added.md index 2b041ac84c..9eb31afe24 100644 --- a/changelog.d/us-medicare-irmaa-holdout-1206.added.md +++ b/changelog.d/us-medicare-irmaa-holdout-1206.added.md @@ -1 +1 @@ -Report held-out Medicare Part B and Part D IRMAA payers and income-related premiums against the 2026 Trustees Report Tables V.E3 and V.E4 in `us_medicare_irmaa_holdout.json`; validation only, never a calibration target or release gate (#1206). +Report held-out Medicare Part B and Part D IRMAA payers and income-related premiums against the 2026 Trustees Report Tables V.E3 and V.E4 in `us_medicare_irmaa_holdout.json`; validation only, never a calibration target or release gate. With no weighted payer (as on the locked policyengine-us 2.2.1 for 2024) it reports `no_payers` and withholds the comparisons (#1206). diff --git a/changelog.d/us-medicare-part-b-premium-target-enrollees-1206.fixed.md b/changelog.d/us-medicare-part-b-premium-target-enrollees-1206.fixed.md index 71fd39839a..d72a10b297 100644 --- a/changelog.d/us-medicare-part-b-premium-target-enrollees-1206.fixed.md +++ b/changelog.d/us-medicare-part-b-premium-target-enrollees-1206.fixed.md @@ -1 +1 @@ -Bind the CMS Part B premiums-from-enrollees target to `gross_medicare_part_b_premium_if_enrolled`, the enrollment-gated gross premium, instead of `gross_medicare_part_b_premium`, which also charged eligible non-enrollees (#1206). +Bind the CMS Part B premiums-from-enrollees target to `gross_medicare_part_b_premium_if_enrolled`, the enrollment-gated gross premium, instead of `gross_medicare_part_b_premium`, which also charged eligible non-enrollees (#1206). On the locked policyengine-us 2.2.1 the bound premium carries no 2024 income-related premium, so the row reads the standard premium alone until Microcosm's engine includes PolicyEngine/policyengine-us#10106. diff --git a/docs/agent-guide.md b/docs/agent-guide.md index f44ce33a4c..1c0e7c48cf 100644 --- a/docs/agent-guide.md +++ b/docs/agent-guide.md @@ -227,11 +227,18 @@ reported explicitly. The report records the difference between SSA's December It also writes `us_medicare_irmaa_holdout.json`: weighted Medicare Part B and Part D income-related premium (IRMAA) payers and annual amounts, compared with the 2026 Medicare Trustees Report's calendar-year rows in Tables V.E3 and V.E4 -(microcosm#1206). Part B counts -`gross_medicare_part_b_premium_if_enrolled - base_part_b_premium` for -`medicare_enrolled` people only, because the base premium is defined for every -eligible person. It is held out the same way: no calibration row, no release -refusal. +(microcosm#1206). Both Parts count `medicare_enrolled` people only (Part A +and/or Part B). Part B is +`gross_medicare_part_b_premium_if_enrolled - base_part_b_premium`, because the +base premium is defined for every eligible person. Part D is +`income_adjusted_part_d_premium_surcharge`, which policyengine-us charges every +Medicare enrollee above the thresholds until PolicyEngine/policyengine-us#10115 +gates it on Part D enrollment. IRMAA rests on income from two years before +the period. The locked policyengine-us 2.2.1 reads that year's inputs, and a +single-year release H5 has none, so a 2024 release reports `status: no_payers` +and withholds the comparisons until the engine includes +PolicyEngine/policyengine-us#10106. It is held out the same way: no +calibration row, no release refusal. Before a release rerun, run the small-H5 guard sweep described in [the release build rule](us-release-build-rule.md#post-export-scoring). That fixture check is separate from full-export timing and release diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/fiscal_targets.py b/packages/microcosm-build/src/microcosm/build/us_runtime/fiscal_targets.py index 33724b6594..7df8db9b95 100644 --- a/packages/microcosm-build/src/microcosm/build/us_runtime/fiscal_targets.py +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/fiscal_targets.py @@ -457,6 +457,11 @@ def _us_hierarchy_seed( # it also charges eligible people who never enrolled. PolicyEngine-US # documents the enrollment-gated gross_medicare_part_b_premium_if_enrolled # for CMS premiums-from-enrollees targets (microcosm#1206). + # The target includes income-related premiums (IRMAA). On the locked + # policyengine-us 2.2.1 a 2024 release has none, because IRMAA rests on + # income two years earlier and the H5 carries 2024 inputs only, so this + # row reads the standard premium alone until Microcosm's engine includes + # PolicyEngine/policyengine-us#10106 (see microcosm#1206 for the sums). ("cms_medicare", "actual_amount", "premiums_from_enrollees"): ( "gross_medicare_part_b_premium_if_enrolled", "cms_medicare", diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/medicare_irmaa_holdout.py b/packages/microcosm-build/src/microcosm/build/us_runtime/medicare_irmaa_holdout.py index 8974069114..eed1575326 100644 --- a/packages/microcosm-build/src/microcosm/build/us_runtime/medicare_irmaa_holdout.py +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/medicare_irmaa_holdout.py @@ -116,9 +116,31 @@ def average_monthly_premium_per_payer(self) -> float: "part_b": ( f"{_PART_B_GROSS} minus {_PART_B_BASE}, for persons with {_ENROLLED}" ), - "part_d": _PART_D_SURCHARGE, + "part_d": f"{_PART_D_SURCHARGE}, for persons with {_ENROLLED}", } ) +#: Every income-related premium is withheld when no enrollee pays one. +_NO_PAYERS_REASON = ( + "No weighted Medicare enrollee pays an income-related premium, so the " + "Trustees comparisons are withheld rather than reported as -100%. The " + "premium for a year rests on modified adjusted gross income from two years " + "earlier. policyengine-us 2.2.1 computes that income from the earlier " + "year's inputs, which a release H5 carrying only the release year's inputs " + "does not supply; PolicyEngine/policyengine-us#10106 adds a fallback to " + "the earliest input year." +) +_NO_ENROLLEES_REASON = ( + "No weighted person is enrolled in Medicare, so the Trustees comparisons " + "are withheld." +) +_GATES = ( + f"{_ENROLLED} covers Part A and/or Part B, so the Part B measure also " + "counts any Part A-only enrollee the engine charges. The Part D measure is " + "the surcharge as the engine gates it: every Medicare enrollee before " + "PolicyEngine/policyengine-us#10115, Part D enrollees after it. A " + "surcharge on a person outside Medicare enrollment is excluded and " + "reported as outside_enrollment_weighted_amount." +) def medicare_irmaa_holdout_unavailable( @@ -149,6 +171,8 @@ def _part_row( amounts: np.ndarray, weights: np.ndarray, benchmark: MedicareIrmaaBenchmark | None, + *, + compare: bool, ) -> dict[str, Any]: payers = amounts > 0 weighted_payers = float(weights[payers].sum()) @@ -161,17 +185,13 @@ def _part_row( "average_monthly_premium_per_payer": ( weighted_premiums / weighted_payers / 12 if weighted_payers else None ), + "benchmark_payers": None, + "benchmark_aggregate_premiums": None, + "benchmark_average_monthly_premium_per_payer": None, + "payers_relative_difference": None, + "aggregate_premiums_relative_difference": None, } if benchmark is None: - row.update( - { - "benchmark_payers": None, - "benchmark_aggregate_premiums": None, - "benchmark_average_monthly_premium_per_payer": None, - "payers_relative_difference": None, - "aggregate_premiums_relative_difference": None, - } - ) return row row.update( { @@ -187,14 +207,15 @@ def _part_row( "benchmark_average_monthly_premium_per_payer": ( benchmark.average_monthly_premium_per_payer ), - "payers_relative_difference": _relative_difference( - weighted_payers, benchmark.beneficiaries_affected - ), - "aggregate_premiums_relative_difference": _relative_difference( - weighted_premiums, benchmark.aggregate_premiums - ), } ) + if compare: + row["payers_relative_difference"] = _relative_difference( + weighted_payers, benchmark.beneficiaries_affected + ) + row["aggregate_premiums_relative_difference"] = _relative_difference( + weighted_premiums, benchmark.aggregate_premiums + ) return row @@ -210,14 +231,20 @@ def medicare_irmaa_holdout_payload( ) -> dict[str, Any]: """Weighted IRMAA payers and annual income-related premiums by Part. - A person's Part B income-related premium is their enrollment-gated gross - Part B premium less the standard premium; ``base_part_b_premium`` is - defined for every Medicare-eligible person, so it is counted only for - ``medicare_enrolled`` people. The Part D amount is the engine's surcharge - as computed. A payer is a person with a positive annual amount. Each - person carries their household's calibrated weight, as supplied by the - written-H5 scorer's person results; the function reads its inputs and - never writes them. + Both Parts count ``medicare_enrolled`` people only. A person's Part B + income-related premium is their enrollment-gated gross Part B premium + less the standard premium; ``base_part_b_premium`` is defined for every + Medicare-eligible person, so a non-enrollee's base premium never counts. + The Part D amount is the engine's surcharge; any surcharge on a + non-enrollee is excluded and reported. A payer is a person with a positive + annual amount. Each person carries their household's calibrated weight, + as supplied by the written-H5 scorer's person results; the function reads + its inputs and never writes them. + + The status is ``available`` when some weighted enrollee pays. With no + weighted enrollee (``no_medicare_enrollees``) or no weighted payer in + either Part (``no_payers``), the model totals are still reported but the + Trustees comparisons are withheld and a reason is given. """ enrolled_values = np.asarray(enrolled) part_b_gross, part_b_base, part_d_surcharge, weights = ( @@ -250,6 +277,18 @@ def medicare_irmaa_holdout_payload( raise ValueError( f"{_PART_B_GROSS} is below {_PART_B_BASE} for an enrolled person." ) + part_d_amount = np.where(enrolled_values, part_d_surcharge, 0.0) + part_d_outside = np.where(enrolled_values, 0.0, part_d_surcharge) + + weighted_enrollees = float(weights[enrolled_values].sum()) + any_payer = bool(weights[(part_b_amount > 0) | (part_d_amount > 0)].sum() > 0) + if weighted_enrollees <= 0: + status, reason = "no_medicare_enrollees", _NO_ENROLLEES_REASON + elif not any_payer: + status, reason = "no_payers", _NO_PAYERS_REASON + else: + status, reason = "available", None + compare = status == "available" benchmarks = MEDICARE_IRMAA_TRUSTEES_2026.get(int(period)) parts = [ @@ -258,24 +297,30 @@ def medicare_irmaa_holdout_payload( part_b_amount, weights, None if benchmarks is None else benchmarks["part_b"], + compare=compare, ), _part_row( "part_d", - part_d_surcharge, + part_d_amount, weights, None if benchmarks is None else benchmarks["part_d"], + compare=compare, ), ] + parts[1]["outside_enrollment_weighted_amount"] = float( + (weights * part_d_outside).sum() + ) payload: dict[str, Any] = { "schema_version": 1, - "status": "available", + "status": status, "period": int(period), "target_role": "validation", "enforced": False, "used_for_calibration": False, "measure": "Medicare income-related premium (IRMAA) payers and amounts", "weight_basis": "person's calibrated household_weight", - "payer": "a person with a positive annual income-related premium", + "payer": "a Medicare enrollee with a positive annual income-related premium", + "weighted_medicare_enrollees": weighted_enrollees, "benchmark": { "source": TRUSTEES_REPORT_2026_TITLE, "source_url": TRUSTEES_REPORT_2026_URL, @@ -298,14 +343,15 @@ def medicare_irmaa_holdout_payload( "aggregate additional premium amounts collected in a calendar " "year; the tables do not state whether the count is an average " "month, a year-end or an ever-in-year count. The model counts " - "persons with a " - "positive annual income-related premium in " - f"{int(period)} and sums those annual amounts. " + "Medicare enrollees with a positive annual income-related premium " + f"in {int(period)} and sums those annual amounts. {_GATES} " "This is a held-out diagnostic, not a calibration target or a " "release gate." ), "parts": parts, } + if reason is not None: + payload["reason"] = reason if release_id is not None: payload["release_id"] = release_id return payload diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_fiscal_refresh_builder.py b/packages/microcosm-build/tests/engine_free/us/test_us_fiscal_refresh_builder.py index d26a88b48d..9adfe409b7 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_fiscal_refresh_builder.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_fiscal_refresh_builder.py @@ -4079,7 +4079,10 @@ def test_pinned_chronicle_feed_compiles_no_unsupported_ledger_filters() -> None: list. That is deliberate — the rows keep pinning the compile the supported/identity classification was reviewed against, and :func:`test_pinned_chronicle_feed_state_surface_compiles_no_unsupported_filters` - is the arm that meets the restated keys on the live feed. + is the arm that meets the restated keys on the live feed. The rows also + predate microcosm#1206, so the Part B premium row still names + ``gross_medicare_part_b_premium``; the live mapping binds + ``gross_medicare_part_b_premium_if_enrolled``. """ builder = _load_builder_module() diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_medicare_irmaa_holdout.py b/packages/microcosm-build/tests/engine_free/us/test_us_medicare_irmaa_holdout.py index 461f1c5f1e..8771d35d9d 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_medicare_irmaa_holdout.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_medicare_irmaa_holdout.py @@ -8,15 +8,19 @@ order, and additive over any split of the persons (household batches). * ``0 <= payers <= weighted enrollees`` for each Part; premiums are nonnegative, and zero when no weight pays. -* Eligible non-enrollees never reach the Part B measure, whatever premium the - engine assigns them. +* Non-enrollees never reach either Part's measure, whatever premium or + surcharge the engine assigns them; a Part D surcharge outside enrollment + is reported, not counted. +* With no weighted enrollee or no weighted payer, the Trustees comparisons + are withheld (never reported as -100%) and a reason is given. * The Trustees rows are validation only: no ledger mapping binds an IRMAA - measure, and the pinned Chronicle feed carries no income-related premium - record set. + measure, the pinned Chronicle feed carries no income-related premium record + set, and only the release tool imports the holdout. """ import json import math +import re from importlib.resources import files import numpy as np @@ -33,6 +37,7 @@ medicare_irmaa_holdout_unavailable, write_medicare_irmaa_holdout, ) +from test_support.paths import paths_for # 2024 standard Part B premium and the tier 1 and tier 5 IRMAA amounts from # Trustees Report Table V.E3 (monthly), and the Part D tier 1 amount (V.E4). @@ -110,6 +115,9 @@ def test_payers_and_premiums_count_only_enrollees_for_part_b(): assert part_d["weighted_payers"] == 2 assert part_d["weighted_aggregate_premiums"] == pytest.approx(2 * _PART_D_TIER_1) assert part_d["benchmark_table"] == "V.E4" + assert part_d["outside_enrollment_weighted_amount"] == 0 + assert payload["weighted_medicare_enrollees"] == 5 + assert "reason" not in payload assert payload["release_id"] == "fixture" assert payload["status"] == "available" assert payload["target_role"] == "validation" @@ -120,6 +128,55 @@ def test_payers_and_premiums_count_only_enrollees_for_part_b(): assert payload["benchmark"]["year"] == 2024 assert payload["benchmark"]["tables"] == ["V.E3", "V.E4"] assert "ever-in-year" in payload["comparability"] + assert "#10115" in payload["comparability"] + + +def test_a_part_d_surcharge_outside_enrollment_is_reported_not_counted(): + payload = _payload( + [True, False], + [_BASE + _PART_B_TIER_1, 0.0], + [_BASE, _BASE], + [_PART_D_TIER_1, _PART_D_TIER_1], + [2.0, 3.0], + ) + part_d = payload["parts"][1] + assert part_d["weighted_payers"] == 2 + assert part_d["weighted_aggregate_premiums"] == pytest.approx(2 * _PART_D_TIER_1) + assert part_d["outside_enrollment_weighted_amount"] == pytest.approx( + 3 * _PART_D_TIER_1 + ) + + +def test_no_weighted_payer_withholds_the_trustees_comparisons(): + """Enrollees but no income-related premium (pe-us 2.2.1 on a 2024 H5).""" + payload = _payload( + [True, True, False], [_BASE, _BASE, 0.0], [_BASE] * 3, [0.0] * 3, [4, 5, 6] + ) + assert payload["status"] == "no_payers" + assert "two years" in payload["reason"] + assert "#10106" in payload["reason"] + assert payload["weighted_medicare_enrollees"] == 9 + for row in payload["parts"]: + assert row["weighted_payers"] == 0 + assert row["weighted_aggregate_premiums"] == 0 + assert row["benchmark_payers"] is not None + assert row["payers_relative_difference"] is None + assert row["aggregate_premiums_relative_difference"] is None + assert payload["enforced"] is False + assert payload["used_for_calibration"] is False + + +def test_no_weighted_enrollee_withholds_the_trustees_comparisons(): + for enrolled, weights in (([False], [1.0]), ([True], [0.0]), ([], [])): + n = len(enrolled) + payload = _payload( + enrolled, [_BASE + _PART_B_TIER_1] * n, [_BASE] * n, [0.0] * n, weights + ) + assert payload["status"] == "no_medicare_enrollees" + assert payload["weighted_medicare_enrollees"] == 0 + for row in payload["parts"]: + assert row["payers_relative_difference"] is None + assert row["aggregate_premiums_relative_difference"] is None def test_numeric_enrollment_flags_are_accepted_and_others_refused(): @@ -280,22 +337,53 @@ def test_payers_and_premiums_are_bounded_by_enrollment(people): @settings(max_examples=200, deadline=None) @given(_people(min_size=1), st.data()) -def test_non_enrollees_never_reach_the_part_b_measure(people, data): - """Whatever base premium a non-enrollee carries leaves Part B unchanged.""" +def test_non_enrollees_never_reach_either_measure(people, data): + """Whatever premiums a non-enrollee carries leave both Parts' totals, + comparisons and status unchanged; their Part D surcharge is reported.""" enrolled, gross, base, part_d, weights = people - reference = _payload(*people)["parts"][0] - noise = np.asarray( - data.draw( - st.lists( - st.floats(0, 1e5, allow_nan=False), - min_size=len(base), - max_size=len(base), + reference = _payload(*people) + + def noise(): + return np.asarray( + data.draw( + st.lists( + st.floats(0, 1e5, allow_nan=False), + min_size=len(base), + max_size=len(base), + ) ) ) + + surcharge = noise() + perturbed = _payload( + enrolled, + np.where(enrolled, gross, noise()), + np.where(enrolled, base, noise()), + np.where(enrolled, part_d, surcharge), + weights, ) - perturbed_base = np.where(enrolled, base, noise) - perturbed = _payload(enrolled, gross, perturbed_base, part_d, weights)["parts"][0] - assert perturbed == reference + assert perturbed["status"] == reference["status"] + assert perturbed["parts"][0] == reference["parts"][0] + outside = perturbed["parts"][1].pop("outside_enrollment_weighted_amount") + reference["parts"][1].pop("outside_enrollment_weighted_amount") + assert perturbed["parts"][1] == reference["parts"][1] + assert outside == pytest.approx( + float((weights * np.where(enrolled, 0.0, surcharge)).sum()), + rel=1e-9, + abs=1e-6, + ) + + +@settings(max_examples=200, deadline=None) +@given(_people()) +def test_comparisons_are_reported_exactly_when_some_enrollee_pays(people): + enrolled, gross, base, part_d, weights = people + payload = _payload(*people) + payers = (gross - base > 0) | (part_d > 0) + pays = bool(weights[enrolled & payers].sum() > 0) + assert (payload["status"] == "available") == pays + for row in payload["parts"]: + assert (row["payers_relative_difference"] is not None) == pays # --------------------------------------------------------------------------- @@ -326,17 +414,23 @@ def test_pinned_chronicle_feed_carries_no_income_related_premium_rows(): scope = json.loads( files("microcosm.build.us").joinpath("chronicle_feed_scope.json").read_text() ) - medicare = { - (pair["package_id"], pair["record_set_id"]) - for pair in scope["pairs"] - if pair["record_set_id"].startswith("cms_medicare") - } - assert medicare == { - ( - "cms-medicare-trustees-report-2025-part-b-premium-income", - "cms_medicare.cy2024.part_b_premium_income", - ) - } + assert scope["pairs"] for pair in scope["pairs"]: text = json.dumps(pair).lower() assert "irmaa" not in text and "income_related" not in text, pair + + +def test_only_the_release_tool_imports_the_holdout(): + """No calibration, registry or gate module can read the Trustees rows.""" + root = paths_for("microcosm-build").repository + pattern = re.compile(r"medicare_irmaa_holdout|MEDICARE_IRMAA_TRUSTEES_2026") + module = "medicare_irmaa_holdout.py" + readers = sorted( + str(path.relative_to(root)) + for directory in ("packages", "tools") + for path in (root / directory).rglob("*.py") + if "/tests/" not in str(path) + and path.name != module + and pattern.search(path.read_text(encoding="utf-8")) + ) + assert readers == ["tools/build_us_fiscal_refresh_release.py"] diff --git a/packages/microcosm-build/tests/engine_scenario/us/test_us_medicare_irmaa_holdout.py b/packages/microcosm-build/tests/engine_scenario/us/test_us_medicare_irmaa_holdout.py index 18badfacb9..ae27f02f28 100644 --- a/packages/microcosm-build/tests/engine_scenario/us/test_us_medicare_irmaa_holdout.py +++ b/packages/microcosm-build/tests/engine_scenario/us/test_us_medicare_irmaa_holdout.py @@ -82,3 +82,41 @@ def test_holdout_measures_follow_the_engine_on_a_household() -> None: assert part_d_row["average_monthly_premium_per_payer"] == pytest.approx( _TRUSTEES_2024_PART_D_TOP_MONTHLY ) + + +def test_a_single_year_household_has_no_irmaa_on_the_locked_engine() -> None: + """Pin today's zero so an engine bump changes it visibly (microcosm#1206). + + A release H5 carries inputs for its year only. On policyengine-us 2.2.1, + IRMAA rests on income two years earlier, which such a household lacks, so + a 2024 enrollee with $600,000 of 2024 wages pays none and the holdout + withholds its Trustees comparisons. A policyengine-us that includes + PolicyEngine/policyengine-us#10106 falls back to the earliest input year; + when Microcosm moves to one, this test fails and should be rewritten to + expect the top tier. + """ + from policyengine_us import Simulation + + person = _person(True) | {"employment_income": {"2024": 600_000}} + situation = { + "people": {"enrollee": person}, + "tax_units": {"tax_unit": {"members": ["enrollee"]}}, + "families": {"family": {"members": ["enrollee"]}}, + "spm_units": {"spm_unit": {"members": ["enrollee"]}}, + "marital_units": {"marital_unit": {"members": ["enrollee"]}}, + "households": { + "household": {"members": ["enrollee"], "state_code": {"2024": "TX"}} + }, + } + simulation = Simulation(situation=situation) + values = [ + np.asarray(simulation.calculate(variable, 2024)) + for variable in MEDICARE_IRMAA_HOLDOUT_VARIABLES + ] + assert simulation.calculate("adjusted_gross_income", 2024)[0] >= 600_000 + payload = medicare_irmaa_holdout_payload(*values, np.array([1.0]), period=2024) + assert payload["status"] == "no_payers", payload["parts"] + assert payload["weighted_medicare_enrollees"] == 1 + for row in payload["parts"]: + assert row["weighted_payers"] == 0 + assert row["payers_relative_difference"] is None From 871451578ef77a59571b990fcba829a1b0da3a1b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 12 Oct 2026 04:52:51 -0400 Subject: [PATCH 5/5] Withhold IRMAA comparisons per Part; tighten holdout wording (#1206) Follow-ups from the approving delta review (review_5570ee8b7.md): - A Part with no weighted payer now withholds its own Trustees comparison (comparison_withheld) even when the other Part has payers, so no Part is ever reported at -100% (e.g. pe-us#10115 before Part D take-up exists). The Hypothesis property checks comparisons per Part. - The no-payers reason says the outcome is expected on policyengine-us 2.2.1 rather than asserting that engine as the cause, and no longer says "rather than -100%" for years with no Trustees row. - The agent guide states the verified outcome: the release builder's batched scorer reports no_payers on the certified populace-us-2024-spm-20260915 file under pe-us 2.2.1 (57.52M weighted enrollees, no payer). - The import guard tests relative path parts, so a checkout under a directory named tests cannot exclude every file. - The changelog says the Part B row's calibration miss widens until the engine includes pe-us#10106. Co-Authored-By: Claude Opus 5.5 --- ...t-b-premium-target-enrollees-1206.fixed.md | 2 +- docs/agent-guide.md | 10 +++-- .../us_runtime/medicare_irmaa_holdout.py | 27 +++++++------ .../us/test_us_medicare_irmaa_holdout.py | 39 +++++++++++++++---- 4 files changed, 54 insertions(+), 24 deletions(-) diff --git a/changelog.d/us-medicare-part-b-premium-target-enrollees-1206.fixed.md b/changelog.d/us-medicare-part-b-premium-target-enrollees-1206.fixed.md index d72a10b297..bf703c6aa1 100644 --- a/changelog.d/us-medicare-part-b-premium-target-enrollees-1206.fixed.md +++ b/changelog.d/us-medicare-part-b-premium-target-enrollees-1206.fixed.md @@ -1 +1 @@ -Bind the CMS Part B premiums-from-enrollees target to `gross_medicare_part_b_premium_if_enrolled`, the enrollment-gated gross premium, instead of `gross_medicare_part_b_premium`, which also charged eligible non-enrollees (#1206). On the locked policyengine-us 2.2.1 the bound premium carries no 2024 income-related premium, so the row reads the standard premium alone until Microcosm's engine includes PolicyEngine/policyengine-us#10106. +Bind the CMS Part B premiums-from-enrollees target to `gross_medicare_part_b_premium_if_enrolled`, the enrollment-gated gross premium, instead of `gross_medicare_part_b_premium`, which also charged eligible non-enrollees (#1206). On the locked policyengine-us 2.2.1 the bound premium carries no 2024 income-related premium, so the row reads the standard premium alone, and its calibration miss widens, until Microcosm's engine includes PolicyEngine/policyengine-us#10106. diff --git a/docs/agent-guide.md b/docs/agent-guide.md index 1c0e7c48cf..7903bdc4da 100644 --- a/docs/agent-guide.md +++ b/docs/agent-guide.md @@ -235,10 +235,12 @@ base premium is defined for every eligible person. Part D is Medicare enrollee above the thresholds until PolicyEngine/policyengine-us#10115 gates it on Part D enrollment. IRMAA rests on income from two years before the period. The locked policyengine-us 2.2.1 reads that year's inputs, and a -single-year release H5 has none, so a 2024 release reports `status: no_payers` -and withholds the comparisons until the engine includes -PolicyEngine/policyengine-us#10106. It is held out the same way: no -calibration row, no release refusal. +single-year release H5 has none, so a 2024 release on that engine reports +`status: no_payers` (the batched scorer gives exactly that on the certified +`populace-us-2024-spm-20260915` file) until the engine includes +PolicyEngine/policyengine-us#10106. A Part with no weighted payer never reports +a Trustees comparison. It is held out the same way: no calibration row, no +release refusal. Before a release rerun, run the small-H5 guard sweep described in [the release build rule](us-release-build-rule.md#post-export-scoring). That fixture check is separate from full-export timing and release diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/medicare_irmaa_holdout.py b/packages/microcosm-build/src/microcosm/build/us_runtime/medicare_irmaa_holdout.py index eed1575326..a2e95d9d28 100644 --- a/packages/microcosm-build/src/microcosm/build/us_runtime/medicare_irmaa_holdout.py +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/medicare_irmaa_holdout.py @@ -119,20 +119,19 @@ def average_monthly_premium_per_payer(self) -> float: "part_d": f"{_PART_D_SURCHARGE}, for persons with {_ENROLLED}", } ) -#: Every income-related premium is withheld when no enrollee pays one. _NO_PAYERS_REASON = ( - "No weighted Medicare enrollee pays an income-related premium, so the " - "Trustees comparisons are withheld rather than reported as -100%. The " - "premium for a year rests on modified adjusted gross income from two years " - "earlier. policyengine-us 2.2.1 computes that income from the earlier " - "year's inputs, which a release H5 carrying only the release year's inputs " - "does not supply; PolicyEngine/policyengine-us#10106 adds a fallback to " - "the earliest input year." + "No weighted Medicare enrollee pays an income-related premium, so no " + "Trustees comparison is reported. The premium for a year rests on " + "modified adjusted gross income from two years earlier. On " + "policyengine-us 2.2.1 this outcome is expected for a release H5 that " + "carries only the release year's inputs, because that engine computes the " + "income from the earlier year's inputs; PolicyEngine/policyengine-us#10106 " + "adds a fallback to the earliest input year." ) _NO_ENROLLEES_REASON = ( - "No weighted person is enrolled in Medicare, so the Trustees comparisons " - "are withheld." + "No weighted person is enrolled in Medicare, so no Trustees comparison is reported." ) +_NO_PART_PAYERS = "No weighted enrollee pays this Part's income-related premium." _GATES = ( f"{_ENROLLED} covers Part A and/or Part B, so the Part B measure also " "counts any Part A-only enrollee the engine charges. The Part D measure is " @@ -174,6 +173,7 @@ def _part_row( *, compare: bool, ) -> dict[str, Any]: + """One Part's totals; comparisons need ``compare`` and a weighted payer.""" payers = amounts > 0 weighted_payers = float(weights[payers].sum()) weighted_premiums = float((weights * amounts).sum()) @@ -209,7 +209,9 @@ def _part_row( ), } ) - if compare: + if compare and not weighted_payers > 0: + row["comparison_withheld"] = _NO_PART_PAYERS + elif compare: row["payers_relative_difference"] = _relative_difference( weighted_payers, benchmark.beneficiaries_affected ) @@ -244,7 +246,8 @@ def medicare_irmaa_holdout_payload( The status is ``available`` when some weighted enrollee pays. With no weighted enrollee (``no_medicare_enrollees``) or no weighted payer in either Part (``no_payers``), the model totals are still reported but the - Trustees comparisons are withheld and a reason is given. + Trustees comparisons are withheld and a reason is given. A Part with no + weighted payer never reports a comparison, whatever the status. """ enrolled_values = np.asarray(enrolled) part_b_gross, part_b_base, part_d_surcharge, weights = ( diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_medicare_irmaa_holdout.py b/packages/microcosm-build/tests/engine_free/us/test_us_medicare_irmaa_holdout.py index 8771d35d9d..e7ec18e9b8 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_medicare_irmaa_holdout.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_medicare_irmaa_holdout.py @@ -376,14 +376,39 @@ def noise(): @settings(max_examples=200, deadline=None) @given(_people()) -def test_comparisons_are_reported_exactly_when_some_enrollee_pays(people): +def test_comparisons_are_reported_exactly_when_that_part_has_a_payer(people): + """A Part compares with the Trustees only when one of its weighted + enrollees pays; no Part is ever reported at -100%.""" enrolled, gross, base, part_d, weights = people payload = _payload(*people) - payers = (gross - base > 0) | (part_d > 0) - pays = bool(weights[enrolled & payers].sum() > 0) - assert (payload["status"] == "available") == pays + part_payers = {"part_b": gross - base > 0, "part_d": part_d > 0} + pays = { + part: bool(weights[enrolled & payers].sum() > 0) + for part, payers in part_payers.items() + } + assert (payload["status"] == "available") == any(pays.values()) for row in payload["parts"]: - assert (row["payers_relative_difference"] is not None) == pays + assert (row["payers_relative_difference"] is not None) == pays[row["part"]] + if row["payers_relative_difference"] is not None: + # A positive weighted payer count; a vanishing weight can still + # round model / benchmark - 1 to exactly -1. + assert row["payers_relative_difference"] >= -1 + assert row["weighted_payers"] > 0 + + +def test_a_part_without_payers_withholds_only_its_own_comparison(): + """Part D all zero (pe-us#10115 with no Part D take-up input yet).""" + payload = _payload( + [True, True], [_BASE + _PART_B_TIER_1, _BASE], [_BASE, _BASE], [0, 0], [2, 3] + ) + part_b, part_d = payload["parts"] + assert payload["status"] == "available" + assert part_b["payers_relative_difference"] == pytest.approx(2 / 4_800_000 - 1) + assert "comparison_withheld" not in part_b + assert part_d["weighted_payers"] == 0 + assert part_d["payers_relative_difference"] is None + assert part_d["aggregate_premiums_relative_difference"] is None + assert part_d["comparison_withheld"].startswith("No weighted enrollee") # --------------------------------------------------------------------------- @@ -426,10 +451,10 @@ def test_only_the_release_tool_imports_the_holdout(): pattern = re.compile(r"medicare_irmaa_holdout|MEDICARE_IRMAA_TRUSTEES_2026") module = "medicare_irmaa_holdout.py" readers = sorted( - str(path.relative_to(root)) + path.relative_to(root).as_posix() for directory in ("packages", "tools") for path in (root / directory).rglob("*.py") - if "/tests/" not in str(path) + if "tests" not in path.relative_to(root).parts and path.name != module and pattern.search(path.read_text(encoding="utf-8")) )