diff --git a/swarmmetrics.py b/swarmmetrics.py index 97f9c6d..47006fc 100644 --- a/swarmmetrics.py +++ b/swarmmetrics.py @@ -25,6 +25,7 @@ class EdgeScore: messages_ba: int = 0 reciprocity_raw: float = 0.0 reciprocity_f3: float = 0.0 # log-transform, half-life weighted + reciprocity_f32: float = 0.0 # F3.2: log(count) * log(1+reciprocity) @dataclass @@ -86,6 +87,11 @@ def score_reciprocity(messages: list[dict], now: Optional[float] = None, """ F3 reciprocity: log(1 + min(a→b, b→a) / max(a→b, b→a)) with half-life decay weighting. + + F3.2 (added 2026-07-27): log(count) * log(1 + reciprocity) + Double-log compression prevents volume from buying back low reciprocity. + Fix proposed by reticuli (Colony): count*log(1+r) lets 100-ping broadcast + outrank 5-message dialogue. log(count)*log(1+r) closes that gap. """ import time if now is None: @@ -131,11 +137,16 @@ def score_reciprocity(messages: list[dict], now: Optional[float] = None, mx_raw = max(ab_r, ba_r) raw_ratio = min(ab_r, ba_r) / mx_raw if mx_raw > 0 else 0.0 + # F3.2: log(count) * log(1 + reciprocity) + total_count = ab_r + ba_r + f32 = math.log(max(total_count, 1)) * f3 # f3 is already log(1+ratio) + es = EdgeScore( source=pair[0], target=pair[1], messages_ab=ab_r, messages_ba=ba_r, reciprocity_raw=raw_ratio, - reciprocity_f3=f3 + reciprocity_f3=f3, + reciprocity_f32=f32 ) edges[pair] = es