Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9703888
Sum published bands in money rows and prove measurement producers
claude Nov 2, 2026
7009dd3
Add per-band source-household support and a measurement_prefit gate
claude Nov 2, 2026
923b4de
Merge the fixed G8-w4d head into hunk A
claude Nov 2, 2026
7bc34fd
Merge hunk A, with the fixed G8-w4d head, into hunk D
claude Nov 2, 2026
a9cc2a9
Read measurement_id from params and check the proof on the compiled g…
claude Nov 2, 2026
41bb6ad
Supply measurement proofs where tests activated factor rows by hand
claude Nov 2, 2026
4d2f3b2
Drop the unreachable member-set comparison in summed rows
claude Nov 2, 2026
38cf1bf
Merge hunk A's fixes into hunk D
claude Nov 2, 2026
303be77
Admit the measurement_prefit gate in gates.json (ruling R2C-12)
claude Nov 2, 2026
687b9c4
Make the clone property run on every example and reach empty bands
claude Nov 2, 2026
b4b884a
State the support diagnostics' membership rule and amount weight
claude Nov 2, 2026
80869fd
Test each support check on its own in measurement_prefit
claude Nov 2, 2026
38ae4a7
Register measurement_prefit in the gate roster pin and drop a literal 2
claude Nov 2, 2026
05bb18c
Refuse a second writer of a measured column (A1)
claude Nov 2, 2026
30ae459
Pin the rounding record and scalar members (A3: OA1, OA3)
claude Nov 2, 2026
5e4c846
Merge the G8-r2c A fix round into D
claude Nov 2, 2026
4c644db
Pin amount-only discrepancy, w0 weights and zero-weight households (D1)
claude Nov 2, 2026
d916ea8
Count EXPAND cells as writers of a measured column (R2C-15)
MaxGhenis Nov 3, 2026
af038d0
Merge the EXPAND writer fix into D
MaxGhenis Nov 3, 2026
bf418af
Pin the chain property's rare branches
MaxGhenis Nov 3, 2026
40079df
Merge the chain property's examples into D
MaxGhenis Nov 3, 2026
91bf4c0
Correct the prefit fake's unit-weight fraction
MaxGhenis Nov 3, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
"input_mass_parity",
"ledger_compile_parity",
"macro_realism",
"measurement_prefit",
"nonconstant_columns",
"nonnegative_columns",
"parity",
Expand Down
191 changes: 188 additions & 3 deletions packages/microcosm-build/src/microcosm/build/reference_intervals.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@

import numpy as np

from microcosm.frame.concepts import SOURCE_HOUSEHOLD_ID_COLUMN
from microcosm.graph.canonical import canonical_json

__all__ = [
Expand All @@ -55,13 +56,16 @@
"IntervalPreparationError",
"IntervalTable",
"alternative_closure",
"band_support",
"boundary_diagnostics",
"classify_records",
"frame_boundary_diagnostics",
"interval_document_sha256",
"interval_indicators",
"interval_input_fingerprint",
"interval_table",
"kish_ess",
"record_source_households",
"scale_interval_document",
"scale_interval_table",
"source_interval_table",
Expand Down Expand Up @@ -127,6 +131,11 @@
_DIAGNOSTIC_FIELDS = frozenset(
{"boundary_window_cents", "alternative_positive_band_closure"}
)
#: Optional diagnostics fields. ``support_household_entity`` names the
#: household entity whose ``source_household_id`` the per-band support
#: diagnostics group contributions by; a table without it reports no support.
_OPTIONAL_DIAGNOSTIC_FIELDS = frozenset({"support_household_entity"})
SUPPORT_SCHEMA_VERSION = 1


class IntervalPreparationError(ValueError):
Expand Down Expand Up @@ -159,6 +168,7 @@ class IntervalTable:
cuts: tuple[Fraction, ...]
scaled: bool
declaration: Mapping[str, object]
support_household_entity: str | None = None


def interval_document_sha256(document: object) -> str:
Expand Down Expand Up @@ -318,8 +328,14 @@ def interval_table(value: object) -> IntervalTable:
if "description" in row:
_text(row["description"], "Interval table description")
diagnostics = _closed(
row["diagnostics"], _DIAGNOSTIC_FIELDS, frozenset(), "interval diagnostics"
row["diagnostics"],
_DIAGNOSTIC_FIELDS,
_OPTIONAL_DIAGNOSTIC_FIELDS,
"interval diagnostics",
)
support_household = diagnostics.get("support_household_entity")
if support_household is not None:
_text(support_household, "Interval support household entity")
window = diagnostics["boundary_window_cents"]
if type(window) is not int or window < 0:
raise ValueError("boundary_window_cents must be a nonnegative integer.")
Expand Down Expand Up @@ -427,6 +443,7 @@ def interval_table(value: object) -> IntervalTable:
cuts=tuple(cuts),
scaled=scaled,
declaration=copy.deepcopy(row),
support_household_entity=support_household,
)
if scaled:
_validate_scaling(table, row)
Expand Down Expand Up @@ -762,8 +779,15 @@ def boundary_diagnostics(
*,
weight_record_ids: Sequence[object],
weight_kind: str,
source_households: Sequence[object] | None = None,
) -> dict[str, object]:
"""Fixed-weight boundary sensitivity, computed with exact weight arithmetic."""
"""Fixed-weight boundary sensitivity, computed with exact weight arithmetic.

``source_households`` gives each record's source household, aligned with
``record_ids``. A table that declares ``support_household_entity`` needs
it and gains per-band support (:func:`band_support`); a table that does
not declare it refuses it, so its report is unchanged.
"""

order = _record_order(record_ids)
if _record_order(weight_record_ids) != order:
Expand All @@ -774,6 +798,11 @@ def boundary_diagnostics(
amounts = list(amounts)
if len(amounts) != len(order):
raise ValueError("Diagnostic amounts must be record-aligned.")
if (table.support_household_entity is None) != (source_households is None):
raise ValueError(
f"Interval table {table.id!r} support diagnostics need source "
"households exactly when the table declares support_household_entity."
)
alternative = alternative_closure(table.closure)
floored = _validated(table, amounts, universe)
zero = Fraction(0)
Expand Down Expand Up @@ -901,8 +930,19 @@ def floored_by_destination(closure):
row["alternative"] = measures(other[index])
row["delta"] = delta(index)
bands.append(row)
if source_households is not None:
support = band_support(table, floored, values.tolist(), source_households)
declared_bands = {band["id"]: band for band in table.declaration["bands"]}
for row in bands:
row["support"] = support[row["band"]]
band = declared_bands[row["band"]]
if "count_reference" in band:
row["references"] = {
"count": band["count_reference"],
"amount": band["amount_reference"],
}
scaling = table.declaration.get("scaling")
return {
report = {
"schema_version": 1,
"kind": "interval_boundary_diagnostics",
"table": table.id,
Expand Down Expand Up @@ -952,6 +992,146 @@ def floored_by_destination(closure):
"publisher_reclassification": PUBLISHER_RECLASSIFICATION,
"limitation": LIMITATION,
}
if source_households is not None:
report["support"] = {
"schema_version": SUPPORT_SCHEMA_VERSION,
"household_entity": table.support_household_entity,
"source_household_column": SOURCE_HOUSEHOLD_ID_COLUMN,
"grouping": "source_household",
}
return report


# ---------------------------------------------------------------------------
# Per-band support (G8-r2c hunk D)
# ---------------------------------------------------------------------------


def kish_ess(contributions: Sequence[Fraction]) -> Fraction:
"""Kish effective sample size ``(sum c)^2 / sum c^2``, exactly; 0 when empty."""

total = sum(contributions, Fraction(0))
squares = sum((value * value for value in contributions), Fraction(0))
return Fraction(0) if squares == 0 else total * total / squares


def _support_measure(groups: Mapping[object, Fraction]) -> dict[str, object]:
contributions = [value for value in groups.values() if value != 0]
ess = kish_ess(contributions)
return {
"source_households": len(contributions),
"kish_ess": _rational(ess),
}


def band_support(
table: IntervalTable,
floored: Sequence[int | None],
weights: Sequence[float],
source_households: Sequence[object],
) -> dict[str, dict[str, object]]:
"""Distinct source households and source-household Kish ESS per band.

Each included record contributes ``w`` to its band's count and ``w * z``
to its band's amount, where ``w`` is its weight as the diagnostics read
it (the frame's weights on the table's entity, the weights entering
calibration) and ``z`` is its floored amount in cents: an amount
contribution's weight is ``w * z``, the record's share of the band's
weighted amount. The
contributions are summed per source household first, so donor clones of
one household add no support, and only then enter the Kish ratio. A
household counts as support only where its summed contribution is
nonzero. Counts and amounts are reported separately.
"""

households = list(source_households)
if len(households) != len(floored) or len(weights) != len(floored):
raise ValueError("Support diagnostics need record-aligned source households.")
if any(
isinstance(item, (bool, np.bool_)) or not isinstance(item, (str, Integral))
for item in households
):
raise ValueError("Source household identities are strings or integers.")
counts = {band: {} for band in table.band_ids}
amounts = {band: {} for band in table.band_ids}
for weight, z, household in zip(weights, floored, households, strict=True):
if z is None:
continue
position = _position(table, z, table.closure)
if position == BELOW_FIRST_CUT:
continue
band = table.band_ids[position]
key = household if isinstance(household, str) else int(household)
exact = Fraction(weight)
counts[band][key] = counts[band].get(key, Fraction(0)) + exact
amounts[band][key] = amounts[band].get(key, Fraction(0)) + exact * z
return {
band: {
"count": _support_measure(counts[band]),
"amount": _support_measure(amounts[band]),
}
for band in table.band_ids
}


def record_source_households(frame, entity: str, household_entity: str) -> list:
"""Each ``entity`` record's source household, through household membership.

The frame declares membership only through the person table: each group
entity has a person column ``EntitySchema.membership_column(group)``
(``microcosm/frame/schema.py``), which ``Frame._group_positions`` reads
(``microcosm/frame/bundle.py``). A person reads its household column
directly. A group unit reads the households of its members, as
``Frame._aggregate_group_to_group`` does when it places one group onto
another, and like it requires the unit to nest in one household: a unit
with no members, or members in several households, has no household
membership and refuses by name. So two units in one household map to
one source household and count once. The household table supplies
``source_household_id``.
"""

schema = frame.schema
refusal = (
f"Interval support: entity {entity!r} has no household membership in "
f"{household_entity!r}"
)
if household_entity not in schema.group_entities:
raise ValueError(f"{refusal}; the frame declares no such household entity.")
households = frame.table(household_entity)
if SOURCE_HOUSEHOLD_ID_COLUMN not in households:
raise ValueError(
f"Interval support needs {household_entity}."
f"{SOURCE_HOUSEHOLD_ID_COLUMN} on the frame."
)
source = dict(
zip(
households[schema.id_column(household_entity)].tolist(),
households[SOURCE_HOUSEHOLD_ID_COLUMN].tolist(),
strict=True,
)
)
if entity == household_entity:
return [source[item] for item in households[schema.id_column(entity)]]
person = frame.table(schema.person_entity)
membership = person[schema.membership_column(household_entity)].tolist()
if entity == schema.person_entity:
return [source[item] for item in membership]
if entity not in schema.group_entities:
raise ValueError(f"{refusal}; it is not a declared entity.")
unit_households: dict[object, set] = {}
for unit, household in zip(
person[schema.membership_column(entity)].tolist(), membership, strict=True
):
unit_households.setdefault(unit, set()).add(household)
result = []
for unit in frame.table(entity)[schema.id_column(entity)].tolist():
found = unit_households.get(unit, set())
if len(found) != 1:
raise ValueError(
f"{refusal}; unit {unit!r} has members in {len(found)} households."
)
result.append(source[next(iter(found))])
return result


def frame_boundary_diagnostics(table: IntervalTable, frame) -> dict[str, object]:
Expand All @@ -973,4 +1153,9 @@ def frame_boundary_diagnostics(table: IntervalTable, frame) -> dict[str, object]
weights.values,
weight_record_ids=ids,
weight_kind=weights.kind.value,
source_households=None
if table.support_household_entity is None
else record_source_households(
frame, table.entity, table.support_household_entity
),
)
Loading
Loading