""" Median computation for vote parameters. Computes the element-wise median of (vinf, a, b, c, d, e) across all active votes. This parametric median is chosen over geometric median because: - It's transparent and politically explainable - The result is itself a valid set of Bézier parameters Post-processing: the median is sanitized to guarantee monotonicity of tier 2. The condition for tier 2 to be non-decreasing is e <= 1/3. If the raw median violates this, e is clamped to E_MAX_MONOTONE. """ import numpy as np from dataclasses import dataclass # Maximum value of e that guarantees tier-2 price monotonicity. # prix_m3_2 = p0 + (pmax-p0) * ((1-3e)t³ + 3e t²) # The cubic is monotone non-decreasing on [0,1] iff (1-3e) >= 0, i.e. e <= 1/3. E_MAX_MONOTONE = 1.0 / 3.0 @dataclass class VoteParams: """The 6 citizen-adjustable parameters.""" vinf: float a: float b: float c: float d: float e: float def _is_tier2_monotone(e: float) -> bool: """Check if tier-2 price curve is monotone non-decreasing.""" return e <= E_MAX_MONOTONE def sanitize_median(params: "VoteParams") -> "VoteParams": """ Ensure the median curve is physically valid: - Tier 2 must be monotone non-decreasing (penalizes high consumption) - If e violates monotonicity, clamp it to E_MAX_MONOTONE """ e = params.e if not _is_tier2_monotone(e): e = E_MAX_MONOTONE return VoteParams( vinf=params.vinf, a=params.a, b=params.b, c=params.c, d=params.d, e=e, ) def compute_median(votes: list[VoteParams]) -> VoteParams | None: """ Compute element-wise median of vote parameters. Returns None if no votes provided. The result is sanitized for tier-2 monotonicity. """ if not votes: return None vinfs = [v.vinf for v in votes] a_s = [v.a for v in votes] b_s = [v.b for v in votes] c_s = [v.c for v in votes] d_s = [v.d for v in votes] e_s = [v.e for v in votes] raw = VoteParams( vinf=float(np.median(vinfs)), a=float(np.median(a_s)), b=float(np.median(b_s)), c=float(np.median(c_s)), d=float(np.median(d_s)), e=float(np.median(e_s)), ) return sanitize_median(raw)