Skip to content
Merged
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
4 changes: 2 additions & 2 deletions SYSTEM_REQUIREMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ A US build is two heavy phases over a sampling Frame, plus loaders:
which is what the imputation benchmark below measures directly.
2. **Calibration** (`populace.calibrate`) — compile targets (national + county
control totals) into a sparse constraint matrix over the pool's weight
vector, then optimize log-weights with torch Adam (the bounded relative-error
loss), optionally with L0 generate-big-then-prune. See
vector, then optimize log-weights with torch Adam (capped weighted MAPE),
optionally with L0 generate-big-then-prune. See
`packages/populace-calibrate/src/populace/calibrate/solve.py`.
3. **Loaders** (`populace.data`) — pull a published population artifact from the
Hugging Face Hub and return it as a policyengine engine dataset.
Expand Down
6 changes: 3 additions & 3 deletions packages/populace-build/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ names its donor survey and fails loudly — no silent fallbacks), and the
- **rotated holdout** — deterministic target folds so *every* target is held
out exactly once across rotations, instead of one lucky split.

All gate losses use the calibrator's bounded relative-error loss
`mean(((est − target)/(target + 1))²)` — scorers consume the same functions,
so there is no calibrator-vs-scorer objective mismatch.
All gate losses use the calibrator's capped weighted-MAPE helper
`weighted_mean(min(abs((estimate − target) / scale), cap))` — scorers consume
the same functions, so there is no calibrator-vs-scorer objective mismatch.

The `us` extra adds the rules engine for formula/export checks. Country source
loaders are not Python dependencies: source stages are declared in packaged
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,9 @@ def _source_loss(
if not errors:
return {
"loss": 0.0,
"loss_formula": "mean(((estimate - target) / (target + 1)) ** 2)",
"loss_formula": (
"mean(min(abs((estimate - target) / max(abs(target), 1)), 10))"
),
"n_columns": 0,
"within_10pct": 1.0,
"max_abs_relative_error": 0.0,
Expand All @@ -401,7 +403,9 @@ def _source_loss(
np.asarray([error["target_total"] for error in errors]),
)
),
"loss_formula": "mean(((estimate - target) / (target + 1)) ** 2)",
"loss_formula": (
"mean(min(abs((estimate - target) / max(abs(target), 1)), 10))"
),
"n_columns": int(len(errors)),
"within_10pct": _json_float(
float(np.mean([abs(error["relative_error"]) <= 0.10 for error in errors]))
Expand Down
17 changes: 15 additions & 2 deletions packages/populace-build/tests/test_gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,14 +210,14 @@ class TestRelativeErrorLoss:
def test_matches_the_calibrator_formula(self) -> None:
est = np.asarray([110.0, 90.0])
tgt = np.asarray([100.0, 100.0])
expected = (((est - tgt) / (tgt + 1.0)) ** 2).mean()
expected = np.abs((est - tgt) / np.maximum(np.abs(tgt), 1.0)).mean()
assert relative_error_loss(est, tgt) == pytest.approx(expected)

def test_accepts_target_loss_weights(self) -> None:
est = np.asarray([110.0, 90.0])
tgt = np.asarray([100.0, 100.0])
weights = np.asarray([10.0, 1.0])
residual = ((est - tgt) / (tgt + 1.0)) ** 2
residual = np.abs((est - tgt) / np.maximum(np.abs(tgt), 1.0))
expected = np.average(residual, weights=weights)

assert relative_error_loss(
Expand All @@ -226,6 +226,19 @@ def test_accepts_target_loss_weights(self) -> None:
target_loss_weights=weights,
) == pytest.approx(expected)

def test_accepts_target_loss_scales_and_caps_each_row(self) -> None:
est = np.asarray([1_000.0, 50.0])
tgt = np.asarray([0.0, 100.0])
scales = np.asarray([100.0, 100.0])
expected = np.asarray([10.0, 0.5]).mean()

assert relative_error_loss(
est,
tgt,
target_loss_scales=scales,
target_loss_cap=10.0,
) == pytest.approx(expected)

def test_shape_mismatch_is_refused(self) -> None:
with pytest.raises(ValueError, match="must align"):
relative_error_loss(np.zeros(2), np.zeros(3))
Expand Down
31 changes: 12 additions & 19 deletions packages/populace-build/tests/test_us_fiscal_refresh_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,18 +403,19 @@ def test_release_gate_failures_reject_missing_critical_targets() -> None:
]


def test_target_value_loss_weights_prioritize_large_targets() -> None:
def test_fiscal_target_loss_weights_prioritize_national_totals() -> None:
builder = _load_builder_module()
registry = TargetRegistry(
(
TargetSpec(
name="small_count",
name="national_income_tax_total",
entity="household",
value=10.0,
source="fixture",
metadata={"target_role": "federal_income_tax_total"},
),
TargetSpec(
name="large_amount",
name="distribution_row",
entity="household",
value=1_000_000.0,
source="fixture",
Expand All @@ -423,33 +424,26 @@ def test_target_value_loss_weights_prioritize_large_targets() -> None:
country="us",
)

weights = builder._target_value_loss_weights(registry)
weights = builder._fiscal_target_loss_weights(registry)

assert weights.shape == (2,)
assert weights.mean() == 1.0
assert weights[1] > weights[0] * 10_000
assert weights[0] == weights[1] * builder.US_NATIONAL_TOTAL_TARGET_LOSS_MULTIPLIER


def test_target_value_loss_weights_boost_ctc_total_role() -> None:
def test_fiscal_target_loss_weights_downweight_state_rows() -> None:
builder = _load_builder_module()
registry = TargetRegistry(
(
TargetSpec(
name="ctc_total",
name="state_role_row",
entity="tax_unit",
value=100.0,
source="fixture",
metadata={"target_role": "ctc_total"},
metadata={"state_fips": "06", "target_role": "tanf_total"},
),
TargetSpec(
name="other_same_value",
entity="tax_unit",
value=100.0,
source="fixture",
metadata={"target_role": "eitc_total"},
),
TargetSpec(
name="legacy_named_ctc_without_role",
name="national_row",
entity="tax_unit",
value=100.0,
source="fixture",
Expand All @@ -458,11 +452,10 @@ def test_target_value_loss_weights_boost_ctc_total_role() -> None:
country="us",
)

weights = builder._target_value_loss_weights(registry)
weights = builder._fiscal_target_loss_weights(registry)

assert weights.mean() == 1.0
assert weights[0] == weights[1] * builder.US_CTC_TARGET_LOSS_WEIGHT_MULTIPLIER
assert weights[2] == weights[1]
assert weights[0] == weights[1] * builder.US_STATE_TARGET_LOSS_MULTIPLIER


def test_release_gate_failures_reject_bad_ctc_fit() -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,9 @@ def test_audit_reports_source_reconstruction_recovery(
old_loss = audit["source_reconstruction_loss"]["old_drop_aggregate"]
disaggregated_loss = audit["source_reconstruction_loss"]["disaggregated"]
assert old_loss["loss"] > 0
assert old_loss["loss_formula"] == "mean(((estimate - target) / (target + 1)) ** 2)"
assert old_loss["loss_formula"] == (
"mean(min(abs((estimate - target) / max(abs(target), 1)), 10))"
)
subset_audit = audit_puf_aggregate_disaggregation(
mini_puf,
seed=42,
Expand All @@ -365,7 +367,7 @@ def test_audit_reports_source_reconstruction_recovery(
)
)
assert old_loss["within_10pct"] < 1.0
assert disaggregated_loss["loss"] < 1e-18
assert disaggregated_loss["loss"] < 1e-12
assert disaggregated_loss["within_10pct"] == pytest.approx(1.0)

agi = next(
Expand Down
9 changes: 5 additions & 4 deletions packages/populace-calibrate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ reproduces them.
Uncompilable targets (missing column, zero `mean` denominator) are **skipped
and reported**, never dropped silently.
3. **Solve for calibrated weights.** `calibrate(frame, targets, ...)` optimizes
the log-weights with torch Adam to minimize the **bounded relative-error
loss** `mean(((A @ w - b)/(b + 1))**2)`. Weights stay strictly positive by
construction (`w = exp(log_w)`). The
result carries a new `Frame` with `CALIBRATED` weights, per-target
the log-weights with torch Adam to minimize **capped weighted MAPE**:
`weighted_mean(min(abs((A @ w - b) / scale), cap))`. By default
`scale = max(abs(target), abs(initial_estimate), 1)` and `cap = 10`
(1000%). Weights stay strictly positive by construction (`w = exp(log_w)`).
The result carries a new `Frame` with `CALIBRATED` weights, per-target
diagnostics, and the loss trajectory.

## Load-bearing options
Expand Down
2 changes: 1 addition & 1 deletion packages/populace-calibrate/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "populace-calibrate"
version = "0.1.0"
description = "The populace representation operator: compile targets into a sparse constraint system over a Frame and solve for calibrated weights (bounded relative-error loss, torch on log-weights, hard max-weight-ratio guard, optional L0 hard-concrete pruning)"
description = "The populace representation operator: compile targets into a sparse constraint system over a Frame and solve for calibrated weights (capped weighted-MAPE loss, torch on log-weights, hard max-weight-ratio guard, optional L0 hard-concrete pruning)"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
produced. Compiles declared facts — population control totals, counts, averages
with standard-error-style tolerances — into a sparse linear constraint system
over a :class:`~populace.frame.Frame`, then solves for the weight vector that
best reproduces them under the bounded relative-error loss
``mean(((A @ w - b)/(b + 1))**2)``, optimized with torch's Adam over the
log-weights (positivity by construction). Multi-period targets stack as
best reproduces them under capped weighted MAPE,
``weighted_mean(min(abs((A @ w - b) / scale), cap))``, optimized with torch's
Adam over the log-weights (positivity by construction). Multi-period targets stack as
``(target, period)`` rows over the *same* weight vector — the charter's "one
weight per trajectory".

Expand Down Expand Up @@ -95,6 +95,7 @@ def _assert_frame_compatible(version: str, required: tuple[int, int]) -> None:
CalibrationResult,
TargetDiagnostic,
calibrate,
default_target_loss_scales,
relative_error_loss,
)
from populace.calibrate.target import ( # noqa: E402 - after the compat gate
Expand All @@ -120,6 +121,7 @@ def _assert_frame_compatible(version: str, required: tuple[int, int]) -> None:
"TargetSpec",
"build_constraint_matrix",
"calibrate",
"default_target_loss_scales",
"diagnostics_payload",
"relative_error_loss",
"specs_from_pe_surface",
Expand Down
31 changes: 1 addition & 30 deletions packages/populace-calibrate/src/populace/calibrate/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,6 @@

__all__ = ["CalibrationProblem", "SkippedTarget", "build_constraint_matrix"]

#: Half-width of the forbidden band around ``-1`` for a compiled target value.
#: The bounded relative-error loss divides each residual by
#: ``(target_value + 1)``; a compiled
#: value within ``_DENOM_EPS`` of ``-1`` drives that denominator to ~0, so the
#: loss, its gradients, and every weight go NaN — surfacing only as the kernel's
#: opaque "Weights must be finite" error. We reject such targets at compile time
#: instead, naming the culprit and the cause.
_DENOM_EPS = 1e-8


@dataclass(frozen=True)
class SkippedTarget:
Expand Down Expand Up @@ -314,30 +305,10 @@ def build_constraint_matrix(
f"({len(skipped)} skipped): {detail}."
)

target_vector = np.asarray(values, dtype=np.float64)
# Guard the loss denominator: the bounded relative-error loss divides each
# residual by ``(target_value + 1)``. A compiled value at -1 (a raw
# ``value=-1``, or a ``mean`` whose ``value`` is exactly 1 below the current
# mean, since its compiled RHS is ``value - current_mean``) makes that
# denominator ~0 -> NaN loss -> NaN gradients -> all-NaN weights, which would
# otherwise surface only as the kernel's opaque "Weights must be finite".
near_minus_one = np.abs(target_vector + 1.0) < _DENOM_EPS
if near_minus_one.any():
culprits = "; ".join(
f"{names[i]} (compiled target value {target_vector[i]:.6g})"
for i in np.flatnonzero(near_minus_one)
)
raise ValueError(
"Target(s) compile to a value at -1, which zeroes the "
"relative-error loss denominator (target_value + 1) and makes every "
f"weight NaN: {culprits}. Shift the target value away from -1 (for a "
"'mean', away from exactly 1 below the current mean)."
)

matrix = sparse.csr_array(np.vstack(rows))
return CalibrationProblem(
matrix=matrix,
target_vector=target_vector,
target_vector=np.asarray(values, dtype=np.float64),
names=tuple(names),
initial_weights=initial,
weight_entity=weight_entity,
Expand Down
Loading
Loading