Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions packages/populace-build/tests/test_us_fiscal_refresh_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,39 @@ def test_release_gate_failures_include_health_input_signal() -> None:
]


def test_base_population_scale_gate_rejects_underweighted_base(small_frame) -> None:
builder = _load_builder_module()

gate = builder._base_population_scale_gate(small_frame)

assert not gate.passed
assert gate.name == "base_population_scale"
assert gate.details["population"] == 6000.0
assert "mass='conserve'" in gate.failures[0]


def test_base_population_scale_gate_accepts_national_scale_base(small_frame) -> None:
builder = _load_builder_module()
benchmark = builder.US_BASE_PERSON_POPULATION_BENCHMARK
frame = small_frame.with_weights(
"household",
builder.Weights(
values=np.asarray([benchmark / 4.0, benchmark / 4.0]),
kind=WeightKind.DESIGN,
),
mass=builder.MassChange(
factor=benchmark / 6000.0,
reason="test fixture national-scale base",
),
)

gate = builder._base_population_scale_gate(frame)

assert gate.passed
assert gate.details["population"] == benchmark
assert gate.details["relative_error"] == 0.0


def test_release_gate_failures_reject_positive_zero_support_targets() -> None:
builder = _load_builder_module()
result = SimpleNamespace(
Expand Down Expand Up @@ -808,6 +841,15 @@ def __len__(self):
passed=True,
details={"requirements_checked": 1},
),
base_population_gate=builder.GateResult(
name="base_population_scale",
passed=True,
details={
"population": 334_200_000.0,
"benchmark": 334_200_000.0,
"relative_error": 0.0,
},
),
health_input_gate=builder.GateResult(
name="health_input_signal",
passed=True,
Expand Down Expand Up @@ -836,6 +878,11 @@ def __len__(self):
"takes_up_aca_if_eligible": 2,
"selected_marketplace_plan_benchmark_ratio": 3,
}
assert build_manifest["gates"]["base_population_scale"]["passed"]
assert (
build_manifest["gates"]["base_population_scale"]["details"]["relative_error"]
== 0.0
)
assert manifest["data_package"] == {"name": "populace-data", "version": "0.1.0"}
assert manifest["default_datasets"] == {"national": "populace_us_2024"}
assert manifest["build"]["built_with_model_package"] == {
Expand Down
81 changes: 81 additions & 0 deletions tools/build_us_fiscal_refresh_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
write_us_source_coverage_diagnostics,
)
from populace.build.us.demographics import (
CENSUS_NATIONAL_AGE_BENCHMARK,
demographics_payload,
population_by_age_from_sim,
write_demographics,
Expand Down Expand Up @@ -74,6 +75,8 @@
US_FISCAL_TARGET_LOSS_CAP = 10.0
US_NATIONAL_TOTAL_TARGET_LOSS_MULTIPLIER = 25.0
US_STATE_TARGET_LOSS_MULTIPLIER = 0.25
US_BASE_PERSON_POPULATION_BENCHMARK = float(sum(CENSUS_NATIONAL_AGE_BENCHMARK.values()))
US_BASE_PERSON_POPULATION_MAX_ABS_RELATIVE_ERROR = 0.25
US_FISCAL_TARGET_ROLE_LOSS_MULTIPLIERS = {
"aca_spending": US_NATIONAL_TOTAL_TARGET_LOSS_MULTIPLIER,
"ctc_total": US_NATIONAL_TOTAL_TARGET_LOSS_MULTIPLIER,
Expand Down Expand Up @@ -1278,6 +1281,47 @@ def _health_input_signal_gate(frame: Frame) -> GateResult:
)


def _base_population_scale_gate(frame: Frame) -> GateResult:
population = float(frame.resolve_weights("person").values.sum())
benchmark = US_BASE_PERSON_POPULATION_BENCHMARK
relative_error = (
(population - benchmark) / benchmark
if math.isfinite(population) and benchmark
else None
)
max_abs = US_BASE_PERSON_POPULATION_MAX_ABS_RELATIVE_ERROR
passed = relative_error is not None and abs(relative_error) <= max_abs
details = {
"measure": "person_weight",
"population": population if math.isfinite(population) else None,
"benchmark": benchmark,
"relative_error": relative_error,
"max_abs_relative_error": max_abs,
"calibration_mass_policy": "conserve",
}
if passed:
return GateResult(
name="base_population_scale",
passed=True,
details=details,
)
if relative_error is None:
failure = "weighted person population is non-finite."
else:
failure = (
f"weighted person population {population:,.0f} differs from Census "
f"benchmark {benchmark:,.0f} by {relative_error:.1%}; release "
"calibration uses mass='conserve', so the base H5 must already be "
"national scale."
)
return GateResult(
name="base_population_scale",
passed=False,
failures=(failure,),
details=details,
)


def _write_npz(path: Path, *, result, registry: TargetRegistry) -> None:
np.savez_compressed(
path,
Expand Down Expand Up @@ -1385,13 +1429,19 @@ def _release_gate_failures(
compilation: Mapping[str, object],
target_profile_gate: GateResult | None = None,
health_input_gate: GateResult | None = None,
base_population_gate: GateResult | None = None,
) -> list[str]:
failures: list[str] = []
if target_profile_gate is not None and not target_profile_gate.passed:
failures.extend(
f"Target profile coverage failed: {failure}"
for failure in target_profile_gate.failures
)
if base_population_gate is not None and not base_population_gate.passed:
failures.extend(
f"Base population scale failed: {failure}"
for failure in base_population_gate.failures
)
if health_input_gate is not None and not health_input_gate.passed:
failures.extend(
f"Health input signal failed: {failure}"
Expand Down Expand Up @@ -1514,12 +1564,14 @@ def _assert_release_gates(
compilation: Mapping[str, object],
target_profile_gate: GateResult | None = None,
health_input_gate: GateResult | None = None,
base_population_gate: GateResult | None = None,
) -> None:
failures = _release_gate_failures(
result,
compilation,
target_profile_gate,
health_input_gate,
base_population_gate,
)
if failures:
raise RuntimeError("Release gates failed: " + "; ".join(failures))
Expand Down Expand Up @@ -1687,6 +1739,7 @@ def _build_manifests(
dropped: Mapping[str, object],
target_profile_gate: GateResult,
health_input_gate: GateResult | None = None,
base_population_gate: GateResult | None = None,
) -> None:
dataset_path = artifact_root / DATASET_FILENAME
calibration_path = artifact_root / CALIBRATION_FILENAME
Expand All @@ -1703,6 +1756,7 @@ def _build_manifests(
dropped,
target_profile_gate,
health_input_gate,
base_population_gate,
)

commit = _git_output("rev-parse", "HEAD")
Expand Down Expand Up @@ -1748,6 +1802,17 @@ def _build_manifests(
"failures": list(target_profile_gate.failures),
"details": dict(target_profile_gate.details),
},
**(
{
"base_population_scale": {
"passed": base_population_gate.passed,
"failures": list(base_population_gate.failures),
"details": dict(base_population_gate.details),
}
}
if base_population_gate is not None
else {}
),
**(
{
"health_input_signal": {
Expand Down Expand Up @@ -1961,6 +2026,15 @@ def main() -> None:
release_dir.mkdir(parents=True, exist_ok=True)

base_frame = _load_frame(base_h5)
base_population_gate = _base_population_scale_gate(base_frame)
if not base_population_gate.passed:
raise RuntimeError(
"Release gates failed: "
+ "; ".join(
f"Base population scale failed: {failure}"
for failure in base_population_gate.failures
)
)
base_frame = _with_aca_marketplace_source_outputs(
base_frame,
target_specs,
Expand Down Expand Up @@ -1995,6 +2069,7 @@ def main() -> None:
compilation,
target_profile_gate,
health_input_gate,
base_population_gate,
)

export_frame = _strip_calibration_columns(base_frame, result.weights)
Expand All @@ -2019,6 +2094,11 @@ def main() -> None:
"failures": list(target_profile_gate.failures),
"details": dict(target_profile_gate.details),
},
"base_population_scale": {
"passed": base_population_gate.passed,
"failures": list(base_population_gate.failures),
"details": dict(base_population_gate.details),
},
"post_export_target_audit": bool(args.audit_export_targets),
},
)
Expand Down Expand Up @@ -2064,6 +2144,7 @@ def main() -> None:
dropped=compilation,
target_profile_gate=target_profile_gate,
health_input_gate=health_input_gate,
base_population_gate=base_population_gate,
)

# Keep a copy of the exact base artifact beside diagnostics for local audit.
Expand Down
Loading