pd: fix a Poisson underflow that broke the quota ceiling at higher cover rates

Found by the cover-traffic sweep: the 99%-safe stake ceiling FELL as the cover
rate rose, while the mean bind rose to 0.38 -- backwards.

quota_exceedance_prob summed the Poisson CDF by hand starting from exp(-lam).
That underflows to zero past lam ~ 745, so the CDF collapsed to 0 and the function
reported every node as exceeding its quota, which drove the bisection in
max_alpha_for_confidence to a meaningless answer. The default rate is unaffected
(lam ~ 32), but raising the cover rate reaches the broken regime immediately,
because the quota and the tolerable block count grow in proportion.

Replaced with scipy poisson.sf. Exceedance at the mean bind is now ~0.5 at every
rate, as it must be, and the safe/mean ratio rises 0.65 -> 0.81 -> 0.90 -> 0.95 ->
0.98 across the swept rates: Poisson noise shrinks relative to a growing quota, so
less headroom is needed for the same confidence. Two regression tests pin both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marcin Pawlowski 2026-08-05 21:02:35 +02:00 committed by Marcin Pawlowski
parent 1865b4110d
commit 8a3a43b366
No known key found for this signature in database
2 changed files with 26 additions and 8 deletions

View File

@ -34,6 +34,7 @@ from __future__ import annotations
import math
import numpy as np
from scipy.stats import poisson
def assign_stake(n_nodes: int, dist: str, rng: np.random.Generator,
@ -179,19 +180,17 @@ def quota_exceedance_prob(alpha: float, f: float, n_nodes: int, slots_per_epoch:
Wins are Binomial(slots_per_epoch, phi(alpha)); the Poisson limit is used, which is accurate
here because phi is tiny and the epoch is long.
Uses scipy's survival function rather than summing the series by hand: ``exp(-lam)`` underflows
to zero past ``lam ~ 745``, which silently collapses a hand-rolled CDF to 0 and reports every
node as exceeding. That regime is reached as soon as the cover rate is raised (the quota, and
with it the tolerable block count, grows in proportion).
"""
lam = expected_blocks_per_epoch(alpha, f, slots_per_epoch)
quota = quota_per_epoch(n_nodes, slots_per_epoch, cover_rate_mult)
k = math.floor(quota)
# P(X > k) for X ~ Poisson(lam), summed up from 0 (k is small in every regime of interest)
if lam <= 0.0:
return 0.0
term = math.exp(-lam)
cdf = term
for i in range(1, k + 1):
term *= lam / i
cdf += term
return max(0.0, min(1.0, 1.0 - cdf))
return float(poisson.sf(math.floor(quota), lam))
def max_alpha_for_confidence(f: float, n_nodes: int, slots_per_epoch: int,

View File

@ -157,3 +157,22 @@ def test_more_cover_traffic_raises_the_ceiling():
rich = simulate_epoch_emissions(s, F, n, S, np.random.default_rng(10), cover_rate_mult=32.0)
assert rich["compliant_frac"] > lean["compliant_frac"]
assert rich["max_compliant_stake"] > lean["max_compliant_stake"]
def test_exceedance_survives_a_large_quota():
"""Regression: exp(-lam) underflows past lam ~ 745, which silently collapsed a hand-rolled
Poisson CDF to 0 and reported every node as exceeding. Raising the cover rate reaches that
regime immediately, so the mean bind must still be a coin flip at every rate."""
n, S = 20_000, 648_000
for rate in (1.0, 16.0, 64.0, 256.0):
p = quota_exceedance_prob(alpha_max(n, F, rate), F, n, S, rate)
assert 0.4 < p < 0.6, (rate, p)
def test_the_safe_ceiling_approaches_the_mean_bind_as_the_quota_grows():
"""Poisson noise shrinks relative to the mean, so the headroom needed for confidence shrinks."""
n, S = 20_000, 648_000
ratios = [max_alpha_for_confidence(F, n, S, 0.99, r) / alpha_max(n, F, r)
for r in (1.0, 16.0, 256.0)]
assert all(b > a for a, b in zip(ratios, ratios[1:], strict=False))
assert ratios[-1] > 0.9