Estate-rule sim: pin the stake arm to heimdall-stake golden vectors #6

Merged
heimdall merged 1 commit from heimdall/stake-vectors into main 2026-11-06 02:32:25 +00:00
3 changed files with 64 additions and 0 deletions

View file

@ -473,6 +473,10 @@ def stake_params(name, overrides=()):
def log_weight(x, b, unit):
"""w = 1 + log_b(x/unit): 1 at x = unit, 0 (not negative) below unit.
Equals CultLib heimdall-stake `weight` with floor 0, cap None and min_amount_per_identity = unit, which
is the cliff at unit; Libby's continuous log below unit is not used. Pinned to her golden vectors by
test_estatesim.StakeVectors (vectors/stake_golden.csv).
This is the weight of ONE IDENTITY. It is not sybil-safe: log is sub-linear, so the same x split over
k identities weighs more (split_weight). Whether the log bounds a purchase depends on identities being
scarce, which nothing in this module supplies. b and unit are PLACEHOLDER values (STAKE_BASE)."""

View file

@ -506,5 +506,36 @@ class HarnessTests(unittest.TestCase):
self.assertEqual(E.verdict((-.01, .05), (0.0, .02)), "undecided")
class StakeVectors(unittest.TestCase):
"""Pins the arm's log_weight / split_weight to CultLib heimdall-stake's golden vectors (PR #9, head 84f932ec,
copied unmodified to vectors/stake_golden.csv). The arm is Libby's function with min_amount_per_identity = unit,
so rows at or above the unit must match; a row below the unit must be 0 in the arm (the cliff) while the
vectors, which set no minimum, give the continuous value. Vectors are integer cents and micro-weights, the
arm is float and unit-agnostic: tolerance is 1 micro-weight per identity (Libby rounds each identity, the arm the total)."""
@staticmethod
def rows():
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "vectors", "stake_golden.csv")
with open(path) as fh:
lines = [l.strip() for l in fh if l.strip() and not l.startswith("#")]
out = [l.split(",") for l in lines]
assert len(out) == 26
return [(k, int(b), int(u), int(a), int(n), int(e)) for k, b, u, a, n, e in out]
def test_every_vector_row(self):
for kind, b, u, a, k, expected in self.rows():
with self.subTest(kind=kind, b=b, unit=u, amount=a, k=k):
if kind == "single":
got = E.log_weight(a, b, u) * 1e6
if a < u:
self.assertEqual(got, 0.0) # arm's cliff = min_amount_per_identity = unit
continue
else:
got = E.split_weight(a, k, b, u)[0] * 1e6
self.assertGreaterEqual(a // k, u) # every split row keeps each identity at or above unit
self.assertLessEqual(abs(got - expected), max(1, k))
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,29 @@
# kind,base,unit_cents,amount_cents,k,expected_micro
# single: weight of one identity holding amount. split: budget (amount) split evenly over k identities (first amount%k hold one extra cent), summed.
# expected values computed independently with 60-digit decimal ln, rounded half up. No floor, cap or minimum.
single,10,100,100,1,1000000
single,10,100,1000,1,2000000
single,10,100,10000,1,3000000
single,10,100,1000000,1,5000000
single,10,100,50,1,698970
single,10,100,10,1,0
single,10,100,1,1,0
single,2,100,400,1,3000000
single,2,100,100,1,1000000
single,2,1,1,1,1000000
single,3,500,123456,1,6014537
single,7,1,987654321,1,11643268
single,10,1,1,1,1000000
single,16,250,99999,1,3160960
split,10,100,10000,1,3000000
split,10,100,10000,2,5397940
split,10,100,10000,10,20000000
split,10,100,10000,100,100000000
split,2,100,10000,1,7643856
split,2,100,10000,2,13287712
split,2,100,10000,10,43219280
split,2,100,10000,100,100000000
split,10,100,100000,1,4000000
split,10,100,100000,2,7397940
split,10,100,100000,10,30000000
split,10,100,100000,100,200000000
1 # kind,base,unit_cents,amount_cents,k,expected_micro
2 # single: weight of one identity holding amount. split: budget (amount) split evenly over k identities (first amount%k hold one extra cent), summed.
3 # expected values computed independently with 60-digit decimal ln, rounded half up. No floor, cap or minimum.
4 single,10,100,100,1,1000000
5 single,10,100,1000,1,2000000
6 single,10,100,10000,1,3000000
7 single,10,100,1000000,1,5000000
8 single,10,100,50,1,698970
9 single,10,100,10,1,0
10 single,10,100,1,1,0
11 single,2,100,400,1,3000000
12 single,2,100,100,1,1000000
13 single,2,1,1,1,1000000
14 single,3,500,123456,1,6014537
15 single,7,1,987654321,1,11643268
16 single,10,1,1,1,1000000
17 single,16,250,99999,1,3160960
18 split,10,100,10000,1,3000000
19 split,10,100,10000,2,5397940
20 split,10,100,10000,10,20000000
21 split,10,100,10000,100,100000000
22 split,2,100,10000,1,7643856
23 split,2,100,10000,2,13287712
24 split,2,100,10000,10,43219280
25 split,2,100,10000,100,100000000
26 split,10,100,100000,1,4000000
27 split,10,100,100000,2,7397940
28 split,10,100,100000,10,30000000
29 split,10,100,100000,100,200000000