"""The empathy metric. Every scenario is graded first on how the worst-off participant fares. The welfare model is deliberately simple, fully deterministic, and documented in docs/empathy-metric.md: welfare(actor) = base_welfare(actor) + (treasury / active_member_count) * treasury_weight [active members only] Base welfare starts at the actor's declared value (default 100) and is adjusted by enacted measures: * receiving a payout adds amount * payout_weight; * being expelled subtracts expelled_penalty (and the actor loses their treasury share by no longer being an active member); * each day a state of emergency is active subtracts emergency_burden_per_day from every active member (the liberty cost of expedited, lower-scrutiny process); * measures may carry explicit harms/benefits payloads quantifying the substantive burden of the measure's content. The harness then reports min-welfare (the Rawlsian floor), the identity of the worst-off actor, and the mean. The scenario's ``empathy.floor`` is the contract: if the worst-off participant lands below it, the scenario does not count as ``blocked`` even when the attack itself failed. """ from __future__ import annotations from typing import Any, Iterable from pydantic import BaseModel, ConfigDict, Field class Weights(BaseModel): model_config = ConfigDict(extra="forbid") treasury_weight: float = Field(default=0.1, ge=0.0) payout_weight: float = Field(default=0.1, ge=0.0) expelled_penalty: float = Field(default=40.0, ge=0.0) emergency_burden_per_day: float = Field(default=0.5, ge=0.0) class WelfareReport(BaseModel): model_config = ConfigDict(extra="forbid") per_actor: dict[str, float] min_welfare: float worst_off: str mean_welfare: float def compute_welfare(members: dict[str, Any], treasury: float, weights: Weights) -> WelfareReport: """Compute the welfare vector over every declared person. ``members`` maps actor id to a state object exposing ``.welfare`` (base) and ``.status`` (one of nonmember/active/expelled/exited). Active members additionally hold a pro-rata claim on the treasury. """ if not members: return WelfareReport(per_actor={}, min_welfare=0.0, worst_off="-", mean_welfare=0.0) active = [m for m in members.values() if m.status == "active"] share = (treasury / len(active)) * weights.treasury_weight if active else 0.0 per: dict[str, float] = {} for mid in sorted(members): m = members[mid] per[mid] = m.welfare + (share if m.status == "active" else 0.0) worst = min(per, key=lambda k: (per[k], k)) return WelfareReport( per_actor=per, min_welfare=per[worst], worst_off=worst, mean_welfare=sum(per.values()) / len(per), )