Estate-rule sim: purchased-stake arm (log weight, splitting adversary, half-influence cap) #5

Merged
heimdall merged 2 commits from heimdall/estate-sim-purchased-stake into main 2026-11-05 19:29:58 +00:00
6 changed files with 554 additions and 3 deletions

View file

@ -1,6 +1,6 @@
# Estate-rule sim: report (2026-11-05)
Question: does the unified estate rule (estate votes decide the estate's vote) dampen populism? Spec: `SPEC.md` (written first, four revisions, each before the results it affects). Numbers: `results.md` (235 cells, 40 replicates x 500 motions each, paired draws). Rerun: `python3 -I run.py` (about 11 minutes on 8 jobs; `--quick` for a smoke; `python3 -I test_estatesim.py` for the 41 tests). Output is byte-identical across reruns and across `--jobs` values.
Question: does the unified estate rule (estate votes decide the estate's vote) dampen populism? Spec: `SPEC.md` (written first, four revisions, each before the results it affects). Numbers: `results.md` (235 cells, 40 replicates x 500 motions each, paired draws). Rerun: `python3 -I run.py` (about 11 minutes on 8 jobs; `--quick` for a smoke; `python3 -I test_estatesim.py` for the 52 tests). Output is byte-identical across reruns and across `--jobs` values.
## Answer
@ -39,6 +39,21 @@ Source: Gölz et al. 2018 (introduction) argue, citing Behrens et al. 2014 (Liqu
Limits specific to this result: copying is triggered only when the holder is over the cap (a standing copy habit is not modeled); copiers copy the holder's sincere ballot and are not strategic; the cost of coordination grows with copiers in practice and is a flat probability here; the copy rate has no empirical value. At copy rates well below .5 the cap partly works; where the break-even lies was not mapped. The design question this leaves is not tuning c: a cap bounds the *ledger*, and a ledger-visible cap is evaded by a ballot copy that costs the delegator nothing but a few keystrokes. Whether the system wants delegation visible and capped, or invisible and uncapped, is a product choice for the operator; nothing is ruled here.
## Purchased stake (E6, SPEC section 5c)
Static weight analysis, not a motion simulation: it measures weight share, not PS or MT. Weight w = 1 + log_b(amount/unit), 0 below unit, per-estate b and unit (defaults b = 10, unit = 10 EUR, my choices, in SPEC 5c). The harness had no contribution mechanism, so contribution hours are a new per-agent input with their own per-estate conversion (defaults b = 10, unit = 10 hours); they add to the euro weight and matter to no buyer result below.
**The working hypothesis, that logarithmic weight bounds buyout power, is only half true here, and the true half is not the log's doing.**
- **Within the estate the log does not bound a million-euro buyer.** Against the harness's 4-patron roll (Pareto(1.16) donations from 10 EUR, 40 seeds) the buyer holds 0.524 +/- 0.008 of Patrons weight and alone decides the Patrons estate (share above one half) in 85% of seeds. The log shrinks the buyer from 100% (linear weight) to about half; it does not take it below a majority of a 4-person estate. This is the harness's own cell (roll 4, unit 10 EUR, b = 10), not a typical one. Swept over unit {1, 10, 100} EUR and roll {4, 40, 400} (40 seeds each, table in results.md) the buyer's mean share of Patrons weight runs from 0.007 to 0.953, and the buyer alone decides the estate in 0 to 100% of seeds depending on the cell (0.85 at the headline cell, 0.65 at roll 40 with unit 100, 1.00 at roll 4 with unit 100, 0.00 in the other six). The roll size, Pareto scale and unit are placeholders; the interval, not the point, is the finding. The weight is per identity, so all of this assumes one identity per buyer (next bullet).
- **At council level the bound comes from the estate weights.** With all four estates live the buyer's council share is 0.131 (equal weights) or 0.052 (minds-heavy), below the Patrons weight W_Patrons by construction; the half-influence cap never binds in 6,480 cell-seeds. Linear weight would give 0.25 and 0.10: the log halves the buyer's share, the estate structure sets its ceiling.
- **The half-influence cap binds only when other estates are silent.** Fraction of (cell, seed) in which council share x >= .5 over 162 cells (b x unit x roll x amount x weights) x 40 seeds: all four live 0.000; Patrons + Minds live 0.044 (all of it the degenerate case where no other patron clears the unit, so there is nothing to cut against: unenforceable); Patrons alone live 0.222 (0.148 of cells bind in every seed, 0.089 of cell-seeds unenforceable). It binds more at small rolls (Patrons alone: 0.669 at roll 4 and b = 2 down to 0.000 at roll 400) and with a smaller log base. The Patrons-alone scenario is the renormalization case of prior section 10 (drop non-voting estates); how likely it is in practice is not a sim output.
- **Splitting wins, by orders of magnitude.** The same 1,000,000 EUR over k identities multiplies weight by 1.9 (k = 2), 8.3 (k = 10), 66.7 (k = 100), 500 (k = 1000), and 16,667 at k = amount/unit = 100,000 (each fake identity buying exactly the unit). The multiplier is (amount/unit) / (1 + log_b(amount/unit)); for b = 10, unit = 10 it is 250 at 10,000 EUR, 2,000 at 100,000 EUR. With k = 10 fake identities the buyer's Patrons-alone share is 0.901, with k = 2 it is 0.676. This refutes the log as a purchase bound unless identities are scarce.
- **The half-influence cap is per identity and splitting evades it.** In the Patrons-alone case the single identity trips the cap in 85% of seeds; at k = 2 no identity does (each holds 0.34) while the joint share is 0.676, in every seed. A cap on a person's influence needs an identity oracle the harness does not have.
- **SPEC names no sybil defence** (the charter's frozen roll, A6, bounds joins after notice, not pre-notice buying of identities), so none is implemented as policy. Three candidates are quantified in results.md and none is chosen: a per-person weight cap does not help (it caps the honest single purchase and the fakes are each below it; 100,000x at cap 1); a raised unit floor reduces the multiplier (250 at 1,000 EUR, 5 at 100,000 EUR, 1.0 only at 1,000,000 EUR, where no purchase under the buyer's amount buys any weight), so it neutralises splitting only by pricing out everyone smaller; a per-identity cost neutralises only at about half the budget (bisection values 49.3%, 49.8%, 49.95% of 10,000, 100,000, 1,000,000 EUR; the threshold is c = amount/2, where a second identity is left with nothing to buy), so it would also tax the honest buyer's single identity at that rate. The choice between pricing identities, verifying people or bounding total weight per verified person is a design fork left for the operator. Nothing here rules it.
Limits specific to E6: the roll, the Pareto scale, b and unit are assumptions; the buyer joins as an additional patron and buys once (no repeated purchase or liquidation); decisiveness is estate-level only, not council-level (prior section 10 shows weight share is not decisiveness); the best split is a grid maximum, a lower bound; hours play no part in a purchase scenario because estate weights are fixed and the buyer sits in Patrons, so an hours buyer (a Member who is paid or who farms hours) is untested and the conversion of hours into weight is just a per-estate parameter whose gaming is not modeled.
## Robustness (E5, E5b)
The sign of the PS effect holds across Users appeal 0.4/0.9/1.4, Users turnout .05/.10/.30 and Minds shared taste .3/.9/1.5. Its size scales with Users appeal loading (-0.025 at .4, -0.067 at 1.4): the rule helps in proportion to how appeal-driven the Users estate is, while its merit loss does not go away (MT gap -0.101 at .4 vs -0.063 at 1.4). If Users are not appeal-driven the rule is nearly all cost.
@ -53,7 +68,15 @@ The sign of the PS effect holds across Users appeal 0.4/0.9/1.4, Users turnout .
## Deviations from the spec text
- E6: the brief said the half-influence cap and an 'E3f cap sweep' were already in the harness. The harness has only the delegation cap of SPEC section 5 (a fraction of cast weight per final holder); the operator cap of prior section 10 (council weight share strictly below one half) was not implemented, so `influence_cap` implements it, once, in `estatesim.py`; the E3f sweep is of the delegation cap and is untouched. E6 is a static weight analysis, not integrated into the motion simulation (no PS or MT for buyers).
- Spec rev 3 added renewal (`renew`) and peer edges (`peer`) after the first full run showed (a) revocation had silently emptied every delegation cell at 500 motions and (b) with only delegates delegating onward, depth was inert. The first run's results were discarded. Renewal and peer defaults are my choices, swept in E3c and E3e.
- Cap basis default is the estate's cast weight, not roll; both are swept.
- E3f: the falsifier was first printed as means only; the paired interval was added to `run.py` before the final run (no number changed). Its materiality bar (-.01) is mine.
- Per-category PS tables are printed only for the non-grid experiments (E1, E2, E4, E5).
## Limits of E6 added after independent review
- Weight in `log_weight`, `stake_weight` and `influence_cap` is per identity. Nothing in the module is sybil-safe; the half-influence cap bounds a ledger entry, not a person.
- The hours channel is unanalysed: no scenario gives a buyer hours, so whether hours can be farmed, bought or split is untested.
- Placeholder parameters (marked PLACEHOLDER in `estatesim.py`): b, unit (euros and hours), the Patron roll sizes, the Pareto scale. Ruled: the half cap. From prior section 4: the Pareto exponent.
- The headline figures (0.524, 0.85, split 1.9x/8.3x/66.7x) are pinned in `test_headline_numbers_are_pinned`.

View file

@ -1,6 +1,6 @@
# Estate-rule sim harness: spec (2026-11-05)
Serves follow-up `heimdall-body:follow_up:chat-mind-20261105T002133411Z-t0`. Prior work: `../2026-10-29-estate-tally-game-theory/` (README.md plus numpy probes in `sims/`). Facts there are cited as "prior §n". This document is written before the code; `estatesim.py` implements it, and where the code had to deviate the deviation is listed at the bottom of `REPORT.md`, not edited in here. Revision 2 (delegation rules) was made before any result was generated. Revision 4 (copy-the-ballot, section 5b) was added after the first reported results and before its own. Revision 3 (renewal, peer edges) was forced by the first full run, whose delegation cells were inert; the first run's results were discarded, not kept.
Serves follow-up `heimdall-body:follow_up:chat-mind-20261105T002133411Z-t0`. Prior work: `../2026-10-29-estate-tally-game-theory/` (README.md plus numpy probes in `sims/`). Facts there are cited as "prior §n". This document is written before the code; `estatesim.py` implements it, and where the code had to deviate the deviation is listed at the bottom of `REPORT.md`, not edited in here. Revision 2 (delegation rules) was made before any result was generated. Revision 4 (copy-the-ballot, section 5b) was added after the first reported results and before its own. Revision 5 (purchased stake, section 5c) was written before its results and touches no earlier section. Revision 3 (renewal, peer edges) was forced by the first full run, whose delegation cells were inert; the first run's results were discarded, not kept.
## 1. Claim under test
@ -91,6 +91,26 @@ Serves `heimdall-body:follow_up:chat-s1946-1946`.
**Experiment E3f.** The delegation cap sweep with copying: alpha {0, .5, 1, 1.5, 2.5, 4} x cap {.10, .15, .20, .25, .30, none} x copy {0, .5, 1}, depth 3, strategic, F = 20, as E3. `copy = 0` reproduces E3 exactly. The depth sweep E3c is unchanged and runs with `copy = 0`; so does every other experiment. Falsifier fixed in advance: the cap "reduces effective concentration once copying is available" if the paired-by-replicate difference (effective Users top-holder share at cap .10 minus no cap) has a 95% interval entirely below -.01 (a drop under one point of share is not called a reduction). Evaluated per alpha and per copy rate by `run.py` and printed at the top of E3f in `results.md`; the cap reduces effective concentration under copying only if this reads "reduces" at `copy = 1`. The first full E3f run printed only means; the interval was added before the final run, and the numbers did not change.
## 5c. Purchased stake (revision 5, written before its results)
Serves the purchased-stake brief (operator relayed: Patron vote power is logarithmic in donation, prior section 4; operator influence strictly below half, ruling `operator-influence-below-half`, prior section 10). Not a motion simulation: a static weight analysis, so it measures weight share and decisiveness, not PS or MT.
**Weight.** A purchase of `amount` EUR weighs w = 1 + log_b(amount/unit): 1 at amount = unit, 0 below unit (below the unit buys no weight, never negative). Each estate has its own conversion, set through `Config.overrides` entries `(estate, key, value)` with keys `euro_b`, `euro_unit`, `hours_b`, `hours_unit` (defaults in `STAKE_BASE`). Only Patrons (euros) and Members (hours) carry stake; Minds are counted once and Users are one-each, so both weigh 1.
**Defaults chosen (assumptions, not rulings; the brief gave none).** Patrons and Members alike: euro b = 10, unit = 10 EUR; hours b = 10, unit = 10 hours. b = 10 reads as "each tenfold purchase adds one vote of weight", the simplest reading of the operator's "logarithmic"; 10 EUR is the smallest ordinary Patron donation in the prior ladder (prior section 4: 10, 50, 200, 5000); 10 hours is roughly a working day or so of contribution. The sweep varies b over {2, 10, 100} and unit over {1, 10, 100} EUR; hours are not swept because no buyer outcome depends on them (see below).
**Contribution hours.** The harness had no contribution mechanism (Members were one-each; prior section 4's Pareto contribution curve was left out of scope in section 4 above). Hours are therefore a new per-agent input with their own per-estate conversion: an estate member's weight is the euro channel plus the hours channel, each through its own (b, unit).
**Electorate.** The buyer joins the Patrons estate as an additional member; the other patrons are `ROLL["Patrons"]` = 4 members (also 40 and 400 as a sensitivity) holding Pareto(1.16) donations from 10 EUR (prior section 4's exponent; the scale is mine). 40 seeds, each a fixed integer. Estate weights are the section 4 weights; the live-estate set is a scenario: all four live, Patrons + Minds live, Patrons alone live (renormalization over live estates, section 2).
**Influence.** Council weight share x = W_Patrons x s / (sum of live W), s the buyer's share of Patrons weight; this is the "weight share" of prior section 10. Decisiveness is reported only at the estate level (buyer alone decides the Patrons estate's vote when s > .5 at q = .5); prior section 10 shows weight share is not decisiveness, and no council-level pivot is computed here.
**Half-influence cap.** The cap named in the brief is the prior section 10 operator cap (weight share strictly below one half), which is *not* the `cap` of section 5 (a per-final-holder delegation cap, a fraction of cast weight, with void excess). The harness had no implementation of the operator cap; `influence_cap` is it: binds iff x >= .5, then cuts the holder's internal weight until x = .5(1 - 1e-9), and reports "unenforceable" when no one else holds weight to cut against. It applies per identity.
**Splitting adversary.** The same total split evenly over k identities: weight k(1 + log_b(amount/(k unit))) against 1 + log_b(amount/unit). Splitting wins because the log is sub-linear: the multiplier approaches (amount/unit)/(1 + log_b(amount/unit)) at k = amount/unit. The sweep reports the multiplier by k, the best k on a geometric grid (a lower bound on the maximum), and a table of defences. **SPEC names no sybil defence** (the charter's frozen roll, A6, stops joins after notice, not the buying of many identities beforehand). Three candidates are quantified, none adopted or chosen: a per-person weight cap, a raised unit floor, a per-identity cost paid out of the budget. They are parameters of `split_weight`, off by default.
**Falsifiers fixed in advance.** (1) Working hypothesis "logarithmic weight bounds buyout power": refuted if a one-million-euro buyer's within-estate share s exceeds 1/2 at the harness's own roll, or if splitting multiplies weight by more than 2 at any k. (2) The half-influence cap is "binding" in a (cell, seed) iff x >= .5; the rate over the sweep is reported per live-estate scenario. (3) The cap is "evaded" by splitting iff the joint share is >= .5 while no single identity's share is.
## 6. Strategic and adaptive agents
A fraction ρ = .25 of ballot holders and of absent undelegated roll members are strategic. Each is myopic about the motion at hand and best-responds to everyone else's current ballots over three rounds in random order:
@ -115,6 +135,7 @@ Paired across rules: one set of random draws per motion feeds all rules (common
- **E3d cap basis:** cast vs roll × c ∈ {.05, .10, .20, .30}, depth 3, α = 1.5.
- **E3f copy-the-ballot:** section 5b.
- **E4 appeal attraction:** β sweep at α = 1.5, c = .2.
- **E6 purchased stake:** section 5c. b x unit x Patrons roll x amount x estate weights x live-estate scenario, 40 seeds; splitting k sweep; candidate defences.
- **E5 robustness:** Users appeal loading b ∈ {.4, .9, 1.4}; Users base turnout {.05, .10, .30}; Minds ICC (s_Minds ∈ {.3, .9, 1.5}). This is the check on the assumed parameters in section 4.
## 8. Pre-registered falsifiers
@ -123,7 +144,7 @@ The claim is **supported** in a cell if the paired difference PS(UNI) − PS(DIR
## 9. Tests (`test_estatesim.py`)
Pin the rules, not the spelling: rule thresholds including ties; chains within depth, over depth (uncast), unlimited, terminated by an intermediate direct vote, looped, dead-ended; scope as overlap of all links; direct override; two-strike revocation; cap void, cap floor, copy-the-ballot (off draws nothing, full copying evades the cap, copied ballot is the holder's, within-cap and no-cap holders see no copying, effective weight follows the copied voter), cap on the final holder, cap basis; determinism (same seed, same bytes); paired draws identical across rules; misreporting never produces an outcome the agent likes less; metrics on hand-built outcome lists.
Pin the rules, not the spelling: rule thresholds including ties; chains within depth, over depth (uncast), unlimited, terminated by an intermediate direct vote, looped, dead-ended; scope as overlap of all links; direct override; two-strike revocation; cap void, cap floor, copy-the-ballot (off draws nothing, full copying evades the cap, copied ballot is the holder's, within-cap and no-cap holders see no copying, effective weight follows the copied voter), cap on the final holder, cap basis; purchased stake (weight 1 at unit and 0 below, per-estate conversion, hours channel, the million-euro buyer's share derived from the config, log versus linear, the operator cap's bind and unenforceable cases, the splitting multiplier against its formula, defences that do and do not neutralise, per-identity cap evasion); determinism (same seed, same bytes); paired draws identical across rules; misreporting never produces an outcome the agent likes less; metrics on hand-built outcome lists.
## 10. Sources

View file

@ -436,3 +436,144 @@ def verdict(dps, dmt):
if dps[0] + dps[1] < 0 and dmt[0] >= -.05:
return "supported"
return "undecided"
# ---------------------------------------------------------------- purchased stake (SPEC section 5c)
#
# A static weight analysis, not a motion simulation: it asks what share of the
# council a purchase buys, not how the buyer votes. The estate weights W and the
# set of live estates come from the same tables the rules above use.
STAKE_KEYS = ("euro_b", "euro_unit", "hours_b", "hours_unit")
# PLACEHOLDER: every number in STAKE_BASE, HALF excepted, is my assumption (SPEC section 5c), not a ruling
# or data. Results are conditional on them; run.py sweeps b and unit, not hours.
# Per-estate conversion of stake to weight. Only Patrons (euros) and Members (contribution hours) carry stake; Minds are
# counted once and Users are one-each, so they have no entry and weigh 1.
STAKE_BASE = {
"Patrons": dict(euro_b=10.0, euro_unit=10.0, hours_b=10.0, hours_unit=10.0),
"Members": dict(euro_b=10.0, euro_unit=10.0, hours_b=10.0, hours_unit=10.0),
}
HALF = .5 # RULED, not a placeholder: operator-influence cap: council weight share strictly below this (prior section 10)
STRICT = 1e-9 # "strictly below": a capped holder sits at c*(1-STRICT)
PARETO_ALPHA = 1.16 # exponent from prior section 4 (Pareto(1.16) ladder); the roll sizes below are PLACEHOLDER
PARETO_XM = 10.0 # PLACEHOLDER: EUR scale, mine; prior section 4's ladder starts at 10
STAKE_SEED = 20261105
def stake_params(name, overrides=()):
"""Per-estate stake parameters; Config.overrides entries (estate, key, value) with a STAKE_KEYS key apply."""
p = dict(STAKE_BASE.get(name, {}))
if p:
for en, k, v in overrides:
if en == name and k in STAKE_KEYS:
p[k] = v
return p
def log_weight(x, b, unit):
"""w = 1 + log_b(x/unit): 1 at x = unit, 0 (not negative) below unit.
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)."""
if b <= 1 or unit <= 0:
raise ValueError("log base must exceed 1 and unit must be positive")
return 0.0 if x < unit else 1.0 + math.log(x / unit, b)
def stake_weight(name, euros=0.0, hours=0.0, overrides=()):
"""One identity's weight: euro channel plus contribution-hours channel, each with the estate's own (b, unit).
Per identity, not per person: splitting evades it (see log_weight). The hours channel is UNANALYSED:
it is added here, but no scenario in run.py gives a buyer hours, so nothing below tests whether hours
can be farmed, bought or split. Its (b, unit) are PLACEHOLDER."""
p = stake_params(name, overrides)
if not p:
return 1.0
return (log_weight(euros, p["euro_b"], p["euro_unit"])
+ log_weight(hours, p["hours_b"], p["hours_unit"]))
def pareto_ladder(n, rng):
"""n ordinary-patron amounts in EUR, Pareto(PARETO_ALPHA) from PARETO_XM."""
return [PARETO_XM * (1.0 - rng.random()) ** (-1.0 / PARETO_ALPHA) for _ in range(n)]
def council_share(w, rest, W, e, live):
"""(within-estate share s, council weight share x) of a holder with weight w in estate e;
rest is everyone else's weight in e; live is the tuple of live estate indices (renormalization)."""
s = w / (w + rest) if w + rest > 0 else 0.0
return s, W[e] * s / sum(W[i] for i in live)
def influence_cap(w, rest, W, e, live, c=HALF):
"""Operator-influence cap (prior section 10, ruling operator-influence-below-half): a holder's
council weight share must be strictly below c. Binds iff x >= c; the holder's internal weight is
then cut until x = c*(1-STRICT). Unenforceable when nobody else holds weight in the estate.
The cap is applied to ONE IDENTITY's weight w. A holder who splits across identities is never cut
(cap evasion, results.md), so this is a bound on a ledger entry, not on a person."""
s, x = council_share(w, rest, W, e, live)
out = dict(s=s, x=x, binds=x >= c - EPS, w_capped=w, s_capped=s, x_capped=x, enforceable=True)
if not out["binds"]:
return out
s_cap = c * (1 - STRICT) * sum(W[i] for i in live) / W[e]
if rest <= 0 or s_cap >= 1:
out["enforceable"] = False
return out
wc = s_cap * rest / (1 - s_cap)
out.update(w_capped=wc, s_capped=s_cap, x_capped=W[e] * s_cap / sum(W[i] for i in live))
return out
def split_weight(amount, k, b, unit, identity_cost=0.0, person_cap=None):
"""Total weight of `amount` EUR split evenly over k identities. identity_cost is spent per identity
out of the budget; person_cap limits each identity's weight. Returns (total, per identity)."""
each = (amount - k * identity_cost) / k
wi = log_weight(each, b, unit) if each > 0 else 0.0
if person_cap is not None:
wi = min(wi, person_cap)
return k * wi, wi
def best_split(amount, b, unit, identity_cost=0.0, person_cap=None):
"""(best total weight, k) over a geometric grid of k up to amount/unit. A grid maximum: a lower bound on the true one."""
kmax = max(1, int(amount // unit))
ks, k = {1, kmax}, 1.0
while k < kmax:
ks.add(max(1, round(k)))
k *= 1.05
best = max((split_weight(amount, k, b, unit, identity_cost, person_cap)[0], -k) for k in ks)
return best[0], -best[1]
def min_neutralising_cost(amount, b, unit, person_cap=None, tol=1e-3):
"""Smallest per-identity cost at which no split beats one identity, found by bisection to relative
tol (a bisection value, not a grid point). Neutral = best split is k = 1 and one identity still has
weight. A doubling search from unit brackets the first neutral cost; assumes the predicate is
monotone inside the bracket. Returns None if no cost below the amount is neutral."""
def neutral(c):
return (split_weight(amount, 1, b, unit, c, person_cap)[0] > 0
and best_split(amount, b, unit, c, person_cap)[1] == 1)
hi = unit
while hi < amount and not neutral(hi):
hi *= 2
if hi >= amount:
return None
lo = hi / 2 if hi > unit else 0.0
while hi - lo > tol * hi:
mid = (lo + hi) / 2
lo, hi = (lo, mid) if neutral(mid) else (mid, hi)
return hi
def stake_ladder(n, s):
"""Seed s's roll of n ordinary patrons: a pure function of (n, s)."""
return pareto_ladder(n, random.Random(STAKE_SEED + 1000 * n + s))
def buyer_runs(amount, n, seeds, W, live, overrides=()):
"""influence_cap result for a buyer of `amount` EUR against each of `seeds` rolls of n ordinary patrons."""
wb = stake_weight("Patrons", euros=amount, overrides=overrides)
return [influence_cap(wb, sum(stake_weight("Patrons", euros=x, overrides=overrides) for x in stake_ladder(n, s)),
W, ESTATES.index("Patrons"), live) for s in range(seeds)]

View file

@ -791,6 +791,94 @@ PS by category threshold:
| minds shared s=.3 | -0.035 ±0.008 | -0.093 ±0.008 | 0.041 ±0.009 | 0.312 ±0.013 | 0.353 ±0.013 | 0.277 ±0.011 | 0.103 | 43.1 | 0.50 | 0.81 | undecided |
| minds shared s=1.5 | -0.020 ±0.008 | -0.081 ±0.008 | 0.055 ±0.010 | 0.312 ±0.013 | 0.367 ±0.014 | 0.292 ±0.010 | 0.103 | 43.1 | 0.50 | 0.81 | undecided |
## E6 purchased stake: logarithmic weight, splitting, and the half-influence cap (SPEC section 5c)
Static weight analysis (no motions are simulated, so no PS or MT): a buyer joins the Patrons estate with one purchase; the 4-patron roll of the harness (and 40, 400) holds Pareto(1.16) donations from 10 EUR, 40 seeds each. Weight w = 1 + log_b(amount/unit), 0 below unit; defaults b = 10, unit = 10 EUR. Share s is the buyer's share of Patrons weight; x is its share of council weight (estate weight W x s over the live estates' weights); the cap binds iff x >= .5.
**Million-euro buyer, defaults, Patrons roll 4 (mean over seeds, 95% interval); linear = weight proportional to amount, the counterfactual the log is meant to beat**
| weights | live estates | s log | x log | x linear | cap binds (log) | cap binds (linear) | estate decided by buyer alone (s > .5) |
|---|---|---|---|---|---|---|---|
| equal | all four estates | 0.524 ±0.008 | 0.131 ±0.002 | 0.250 ±0.000 | 0.00 | 0.00 | 0.85 |
| equal | Patrons + Minds | 0.524 ±0.008 | 0.262 ±0.004 | 0.500 ±0.000 | 0.00 | 0.00 | 0.85 |
| equal | Patrons alone | 0.524 ±0.008 | 0.524 ±0.008 | 1.000 ±0.000 | 0.85 | 1.00 | 0.85 |
| minds-heavy | all four estates | 0.524 ±0.008 | 0.052 ±0.001 | 0.100 ±0.000 | 0.00 | 0.00 | 0.85 |
| minds-heavy | Patrons + Minds | 0.524 ±0.008 | 0.105 ±0.002 | 0.200 ±0.000 | 0.00 | 0.00 | 0.85 |
| minds-heavy | Patrons alone | 0.524 ±0.008 | 0.524 ±0.008 | 1.000 ±0.000 | 0.85 | 1.00 | 0.85 |
**Million-euro buyer's share of Patrons weight, b = 10, by unit and roll (mean over seeds, 95% interval; share of seeds in which the buyer alone decides the estate)**
| unit EUR | roll 4 | roll 40 | roll 400 |
|---|---|---|---|
| 1 | 0.425 ±0.005 (0.00) | 0.069 ±0.000 (0.00) | 0.007 ±0.000 (0.00) |
| 10 | 0.524 ±0.008 (0.85) | 0.098 ±0.001 (0.00) | 0.011 ±0.000 (0.00) |
| 100 | 0.953 ±0.028 (1.00) | 0.579 ±0.040 (0.65) | 0.118 ±0.004 (0.00) |
Interval over the swept cells: s from 0.007 to 0.953. The 0.524 headline is the harness's own cell (roll 4, unit 10), not a typical one: the buyer's majority of the estate depends on how few other patrons there are.
**Cap-binding rate over the sweep (b, unit, roll, amount, weights x live set; every cell run on every seed)**
| live estates | cells | cells x seeds | binding (cell, seed) | cells binding in every seed | cells never binding | binding but unenforceable |
|---|---|---|---|---|---|---|
| all four estates | 162 | 6480 | 0.000 | 0.000 | 1.000 | 0.000 |
| Patrons + Minds | 162 | 6480 | 0.044 | 0.000 | 0.889 | 0.044 |
| Patrons alone | 162 | 6480 | 0.222 | 0.148 | 0.704 | 0.089 |
Binding rate in the Patrons-alone scenario by Patrons roll and by log base (fraction of cells x seeds):
| roll | b=2 | b=10 | b=100 |
|---|---|---|---|
| 4 | 0.669 | 0.447 | 0.333 |
| 40 | 0.286 | 0.169 | 0.094 |
| 400 | 0.000 | 0.000 | 0.000 |
**Splitting: the same total split over k identities (defaults, one honest identity = k 1). Multiplier = total weight of the split / weight of the single purchase**
| amount EUR | single weight | k=2 | k=10 | k=100 | k=1000 | best k (grid) | best multiplier |
|---|---|---|---|---|---|---|---|
| 10000 | 4.00 | 1.8 | 7.5 | 50.0 | 250.0 | 1000 | 250 |
| 100000 | 5.00 | 1.9 | 8.0 | 60.0 | 400.0 | 10000 | 2000 |
| 1e+06 | 6.00 | 1.9 | 8.3 | 66.7 | 500.0 | 100000 | 16667 |
Best multiplier at 1,000,000 EUR by log base and unit (b rows, unit columns):
| b | unit=1 | unit=10 | unit=100 |
|---|---|---|---|
| 2 | 50711 (k=740348) | 6027 (k=74738) | 743 (k=7185) |
| 10 | 142857 (k=1000000) | 16667 (k=100000) | 2000 (k=10000) |
| 100 | 250000 (k=1000000) | 28571 (k=100000) | 3333 (k=10000) |
**Candidate defences, none adopted by SPEC (best multiplier at 1,000,000 EUR, defaults)**
| defence | setting | best multiplier | neutralises (best split is k = 1) |
|---|---|---|---|
| per-person weight cap | 1.0 | 100000 | no |
| per-person weight cap | 3.0 | 33333 | no |
| per-person weight cap | 6.0 | 16667 | no |
| unit floor | 10 EUR | 16666.7 | no |
| unit floor | 1000 EUR | 250.0 | no |
| unit floor | 100000 EUR | 5.0 | no |
| unit floor | 500000 EUR | 1.5 | no |
| unit floor | 1e+06 EUR | 1.0 | yes |
| identity cost | smallest cost by bisection, amount 10000 EUR | | yes at 4930 EUR per identity (49.3% of the budget, bisection to 0.1%) |
| identity cost | smallest cost by bisection, amount 100000 EUR | | yes at 49800 EUR per identity (49.8% of the budget, bisection to 0.1%) |
| identity cost | smallest cost by bisection, amount 1e+06 EUR | | yes at 499520 EUR per identity (50.0% of the budget, bisection to 0.1%) |
**Cap evasion: the cap is per identity. Patrons alone, equal weights, roll 4, defaults, 1,000,000 EUR over k identities**
| k | joint share x | share of one identity | cap binds on an identity | joint x >= .5 (evaded) |
|---|---|---|---|---|
| 1 | 0.524 ±0.008 | 0.524 ±0.008 | 0.85 | 0.00 |
| 2 | 0.676 ±0.007 | 0.338 ±0.004 | 0.00 | 1.00 |
| 10 | 0.901 ±0.003 | 0.090 ±0.000 | 0.00 | 1.00 |
| 100 | 0.986 ±0.000 | 0.010 ±0.000 | 0.00 | 1.00 |
**Contribution hours: Members weight from hours alone (no mechanism for contribution existed in the harness; new per-agent input, own per-estate conversion)**
| hours | 5 | 10 | 100 | 1000 | 10000 |
|---|---|---|---|---|---|
| weight | 0.00 | 1.00 | 2.00 | 3.00 | 4.00 |
## Claim tally over all cells (SPEC section 8)
supported 0, fails 2, undecided 233, n/a 0

View file

@ -4,6 +4,7 @@ Writes a deterministic markdown results file (no timestamps). See SPEC.md sectio
"""
import argparse
import os
import random
import sys
from multiprocessing import Pool
@ -209,6 +210,149 @@ def copy_reading(cells, res):
return L + [""]
PATRONS = E.ESTATES.index("Patrons")
LIVE = {"all four estates": (0, 1, 2, 3), "Patrons + Minds": (0, PATRONS), "Patrons alone": (PATRONS,)}
def _stake_rest(overrides, ladder):
"""Weight of the ordinary patrons under the estate's own euro conversion."""
return sum(E.stake_weight("Patrons", euros=x, overrides=overrides) for x in ladder)
def stake_section(quick):
"""E6 purchased stake (SPEC section 5c). Pure functions of fixed seeds; no wall clock."""
seeds = 4 if quick else 40
amounts = (1e6,) if quick else (1e4, 1e5, 1e6)
bs, units, rolls = ((10.0,), (10.0,), (4, 40)) if quick else ((2.0, 10.0, 100.0), (1.0, 10.0, 100.0), (4, 40, 400))
EUR = 1e6
L = ["## E6 purchased stake: logarithmic weight, splitting, and the half-influence cap (SPEC section 5c)", "",
"Static weight analysis (no motions are simulated, so no PS or MT): a buyer joins the Patrons estate with one purchase; "
f"the {E.ROLL['Patrons']}-patron roll of the harness (and 40, 400) holds Pareto({E.PARETO_ALPHA}) donations from {E.PARETO_XM:.0f} EUR, "
f"{seeds} seeds each. Weight w = 1 + log_b(amount/unit), 0 below unit; defaults b = {E.STAKE_BASE['Patrons']['euro_b']:.0f}, "
f"unit = {E.STAKE_BASE['Patrons']['euro_unit']:.0f} EUR. Share s is the buyer's share of Patrons weight; x is its share of council weight "
"(estate weight W x s over the live estates' weights); the cap binds iff x >= .5.", ""]
ladders = {(n, s): E.stake_ladder(n, s) for n in rolls for s in range(seeds)}
# headline: the million-euro buyer, defaults, roll of the harness
n0 = E.ROLL["Patrons"]
L += ["**Million-euro buyer, defaults, Patrons roll " + str(n0) + " (mean over seeds, 95% interval); linear = weight proportional to amount, the counterfactual the log is meant to beat**", "",
"| weights | live estates | s log | x log | x linear | cap binds (log) | cap binds (linear) | estate decided by buyer alone (s > .5) |",
"|---|---|---|---|---|---|---|---|"]
for wn, W in E.WEIGHTS.items():
for ln, live in LIVE.items():
rows = []
for s in range(seeds):
lad = ladders[(n0, s)]
wb = E.stake_weight("Patrons", euros=EUR)
lo = E.influence_cap(wb, _stake_rest((), lad), W, PATRONS, live)
li = E.influence_cap(EUR / 10.0, sum(x / 10.0 for x in lad), W, PATRONS, live)
rows.append((lo["s"], lo["x"], li["x"], lo["binds"], li["binds"], lo["s"] > .5))
m = [E.mean_ci([r[k] for r in rows]) for k in range(3)]
fr = [sum(r[k] for r in rows) / seeds for k in (3, 4, 5)]
L.append(f"| {wn} | {ln} | {f(*m[0])} | {f(*m[1])} | {f(*m[2])} | {fr[0]:.2f} | {fr[1]:.2f} | {fr[2]:.2f} |")
L.append("")
# the headline share is one (unit, roll) cell: sweep both, state the interval
L += ["**Million-euro buyer's share of Patrons weight, b = 10, by unit and roll (mean over seeds, 95% interval; share of seeds in which the buyer alone decides the estate)**", "",
"| unit EUR | " + " | ".join(f"roll {n}" for n in rolls) + " |", "|---|" + "---|" * len(rolls)]
cells = []
for u in units:
cols = []
for n in rolls:
r = E.buyer_runs(EUR, n, seeds, E.WEIGHTS["equal"], (PATRONS,), (("Patrons", "euro_b", 10.0), ("Patrons", "euro_unit", u)))
m = E.mean_ci([x["s"] for x in r])
cells.append(m[0])
cols.append(f"{f(*m)} ({sum(x['s'] > .5 for x in r) / seeds:.2f})")
L.append(f"| {u:g} | " + " | ".join(cols) + " |")
L += ["", f"Interval over the swept cells: s from {min(cells):.3f} to {max(cells):.3f}. The 0.524 headline is the harness's own cell (roll {n0}, unit 10), "
"not a typical one: the buyer's majority of the estate depends on how few other patrons there are.", ""]
# cap-binding rate across the whole sweep
tally = {}
for wn, W in E.WEIGHTS.items():
for ln, live in LIVE.items():
for b in bs:
for u in units:
ov = (("Patrons", "euro_b", b), ("Patrons", "euro_unit", u))
for n in rolls:
for a in amounts:
hit = unf = 0
for s in range(seeds):
lad = ladders[(n, s)]
r = E.influence_cap(E.stake_weight("Patrons", euros=a, overrides=ov), _stake_rest(ov, lad), W, PATRONS, live)
hit += r["binds"]
unf += r["binds"] and not r["enforceable"]
tally.setdefault(ln, []).append((hit, unf, seeds, n, b, a))
L += ["**Cap-binding rate over the sweep (b, unit, roll, amount, weights x live set; every cell run on every seed)**", "",
"| live estates | cells | cells x seeds | binding (cell, seed) | cells binding in every seed | cells never binding | binding but unenforceable |",
"|---|---|---|---|---|---|---|"]
for ln, t in tally.items():
tot = sum(x[2] for x in t)
L.append(f"| {ln} | {len(t)} | {tot} | {sum(x[0] for x in t) / tot:.3f} | "
f"{sum(1 for x in t if x[0] == x[2]) / len(t):.3f} | {sum(1 for x in t if x[0] == 0) / len(t):.3f} | "
f"{sum(x[1] for x in t) / tot:.3f} |")
L += ["", "Binding rate in the Patrons-alone scenario by Patrons roll and by log base (fraction of cells x seeds):", "",
"| roll | " + " | ".join(f"b={b:g}" for b in bs) + " |", "|---|" + "---|" * len(bs)]
for n in rolls:
cols = []
for b in bs:
t = [x for x in tally["Patrons alone"] if x[3] == n and x[4] == b]
cols.append(f"{sum(x[0] for x in t) / sum(x[2] for x in t):.3f}")
L.append(f"| {n} | " + " | ".join(cols) + " |")
L.append("")
# splitting
L += ["**Splitting: the same total split over k identities (defaults, one honest identity = k 1). Multiplier = total weight of the split / weight of the single purchase**", "",
"| amount EUR | single weight | " + " | ".join(f"k={k}" for k in (2, 10, 100, 1000)) + " | best k (grid) | best multiplier |",
"|---|---|" + "---|" * 6]
b0, u0 = E.STAKE_BASE["Patrons"]["euro_b"], E.STAKE_BASE["Patrons"]["euro_unit"]
for a in amounts:
w1 = E.log_weight(a, b0, u0)
bt, bk = E.best_split(a, b0, u0)
L.append(f"| {a:g} | {w1:.2f} | " + " | ".join(
f"{E.split_weight(a, k, b0, u0)[0] / w1:.1f}" if a / k >= u0 else "0 (below unit)" for k in (2, 10, 100, 1000)) +
f" | {bk} | {bt / w1:.0f} |")
L += ["", "Best multiplier at 1,000,000 EUR by log base and unit (b rows, unit columns):", "",
"| b | " + " | ".join(f"unit={u:g}" for u in units) + " |", "|---|" + "---|" * len(units)]
for b in bs:
cols = []
for u in units:
bt, bk = E.best_split(EUR, b, u)
cols.append(f"{bt / E.log_weight(EUR, b, u):.0f} (k={bk})")
L.append(f"| {b:g} | " + " | ".join(cols) + " |")
# defences, candidates only
L += ["", "**Candidate defences, none adopted by SPEC (best multiplier at 1,000,000 EUR, defaults)**", "",
"| defence | setting | best multiplier | neutralises (best split is k = 1) |", "|---|---|---|---|"]
w1 = E.log_weight(EUR, b0, u0)
for wc in (1.0, 3.0, w1):
s1 = min(w1, wc)
bt, bk = E.best_split(EUR, b0, u0, person_cap=wc)
L.append(f"| per-person weight cap | {wc:.1f} | {bt / s1:.0f} | {'yes' if bk == 1 else 'no'} |")
for uf in (10.0, 1e3, 1e5, 5e5, 1e6):
s1 = E.log_weight(EUR, b0, uf)
bt, bk = E.best_split(EUR, b0, uf)
L.append(f"| unit floor | {uf:g} EUR | {bt / s1:.1f} | {'yes' if bk == 1 else 'no'} |")
for a in amounts:
mc = E.min_neutralising_cost(a, b0, u0)
L.append(f"| identity cost | smallest cost by bisection, amount {a:g} EUR | | "
+ (f"yes at {mc:.0f} EUR per identity ({mc / a:.1%} of the budget, bisection to 0.1%)" if mc else "no cost below the amount") + " |")
# cap evasion by splitting, Patrons alone
L += ["", "**Cap evasion: the cap is per identity. Patrons alone, equal weights, roll " + str(n0) + ", defaults, 1,000,000 EUR over k identities**", "",
"| k | joint share x | share of one identity | cap binds on an identity | joint x >= .5 (evaded) |", "|---|---|---|---|---|"]
W = E.WEIGHTS["equal"]
for k in (1, 2, 10, 100):
rows = []
for s in range(seeds):
rest = _stake_rest((), ladders[(n0, s)])
tot, wi = E.split_weight(EUR, k, b0, u0)
joint = E.influence_cap(tot, rest, W, PATRONS, (3,))
one = E.influence_cap(wi, rest + tot - wi, W, PATRONS, (3,))
rows.append((joint["x"], one["x"], one["binds"], joint["x"] >= E.HALF and not one["binds"]))
L.append(f"| {k} | {f(*E.mean_ci([r[0] for r in rows]))} | {f(*E.mean_ci([r[1] for r in rows]))} | "
f"{sum(r[2] for r in rows) / seeds:.2f} | {sum(r[3] for r in rows) / seeds:.2f} |")
# hours channel
L += ["", "**Contribution hours: Members weight from hours alone (no mechanism for contribution existed in the harness; new per-agent input, own per-estate conversion)**", "",
"| hours | " + " | ".join(f"{h:g}" for h in (5, 10, 100, 1000, 10000)) + " |", "|---|---|---|---|---|---|",
"| weight | " + " | ".join(f"{E.stake_weight('Members', hours=h):.2f}" for h in (5, 10, 100, 1000, 10000)) + " |", ""]
return L
def main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("--quick", action="store_true")
@ -243,6 +387,7 @@ def main(argv=None):
for (name, _), r in zip(cells, res):
L += [f"### {title.split(' ')[0]} / {name}", ""] + rule_table(r) + [""]
L += [pair_line(r), "", "PS by category threshold:", ""] + percat_table(r) + [""]
L += stake_section(a.quick)
L += ["## Claim tally over all cells (SPEC section 8)", "",
f"supported {tally['supported']}, fails {tally['fails']}, undecided {tally['undecided']}, n/a {tally['n/a']}", ""]
if failing:

View file

@ -1,4 +1,5 @@
"""python3 -I test_estatesim.py"""
import math
import os
import random
import sys
@ -284,6 +285,138 @@ class CopyTests(unittest.TestCase):
self.assertAlmostEqual(sum(c[7] for c in conc), sum(c[0] for c in conc))
class StakeTests(unittest.TestCase):
"""SPEC section 5c: purchased stake, splitting, the half-influence cap."""
P = E.ESTATES.index("Patrons")
ALL = (0, 1, 2, 3)
def cfg(self):
return E.STAKE_BASE["Patrons"]
def test_weight_is_one_at_unit_zero_below_and_logarithmic(self):
c = self.cfg()
b, u = c["euro_b"], c["euro_unit"]
self.assertEqual(E.log_weight(u, b, u), 1.0)
self.assertEqual(E.log_weight(u * .99, b, u), 0.0) # below unit: no weight, not negative
self.assertAlmostEqual(E.log_weight(u * b ** 3, b, u), 4.0) # each factor b adds one
self.assertLess(E.log_weight(10 * u, b, u), 10) # sub-linear
def test_conversion_is_per_estate_and_overridable(self):
ov = (("Patrons", "euro_b", 2.0), ("Patrons", "euro_unit", 5.0), ("Members", "euro_b", 100.0))
self.assertAlmostEqual(E.stake_weight("Patrons", euros=40, overrides=ov), 1 + math.log(8, 2))
self.assertAlmostEqual(E.stake_weight("Patrons", euros=100), 1 + math.log(10, 10))
self.assertEqual(E.stake_params("Members", ov)["euro_b"], 100.0)
self.assertEqual(E.stake_params("Patrons", ov)["euro_b"], 2.0)
self.assertEqual(E.stake_weight("Users", euros=1e6), 1.0) # no stake channel: one-each
self.assertEqual(E.stake_weight("Minds", hours=1e6), 1.0)
def test_hours_add_to_euros_with_their_own_conversion(self):
ov = (("Members", "hours_b", 2.0), ("Members", "hours_unit", 4.0))
h = E.stake_weight("Members", hours=32, overrides=ov)
self.assertAlmostEqual(h, 1 + math.log(8, 2))
self.assertAlmostEqual(E.stake_weight("Members", euros=100, hours=32, overrides=ov), h + 2.0)
def test_million_euro_buyer_share_is_derived_from_config(self):
c = self.cfg()
wb = E.stake_weight("Patrons", euros=1e6)
n = E.ROLL["Patrons"] # the harness's patron roll, each at exactly the unit
others = n * E.log_weight(c["euro_unit"], c["euro_b"], c["euro_unit"])
s_star = wb / (wb + others)
for wn, W in E.WEIGHTS.items():
# all estates live: the buyer's council share cannot exceed the Patrons estate weight, below half by the charter
r = E.influence_cap(wb, others, W, self.P, self.ALL)
self.assertAlmostEqual(r["s"], s_star)
self.assertAlmostEqual(r["x"], W[self.P] / sum(W) * s_star)
self.assertLess(r["x"], W[self.P] / sum(W) + 1e-12)
self.assertFalse(r["binds"], wn)
# Patrons the only live estate: the cap binds exactly when the buyer outweighs the rest
r = E.influence_cap(wb, others, W, self.P, (self.P,))
self.assertEqual(r["binds"], s_star >= E.HALF)
self.assertEqual(r["binds"], wb >= others)
if r["binds"]:
self.assertLess(r["x_capped"], E.HALF)
self.assertAlmostEqual(r["x_capped"], E.HALF, places=6)
def test_log_weight_gives_a_smaller_share_than_linear_but_still_a_near_total_one_in_a_tiny_estate(self):
c = self.cfg()
rest_amounts = [10.0, 50.0, 200.0, 5000.0]
W = E.WEIGHTS["equal"]
lg = E.council_share(E.stake_weight("Patrons", euros=1e6),
sum(E.stake_weight("Patrons", euros=a) for a in rest_amounts), W, self.P, (self.P,))[1]
li = E.council_share(1e6 / c["euro_unit"], sum(a / c["euro_unit"] for a in rest_amounts), W, self.P, (self.P,))[1]
self.assertLess(lg, li)
self.assertGreater(li, .99)
def test_cap_is_unenforceable_when_nobody_else_holds_weight(self):
r = E.influence_cap(5.0, 0.0, E.WEIGHTS["equal"], self.P, (self.P,))
self.assertTrue(r["binds"])
self.assertFalse(r["enforceable"])
def test_splitting_multiplier_matches_the_formula_and_exceeds_one(self):
c = self.cfg()
b, u, a, k = c["euro_b"], c["euro_unit"], 1e6, 100
tot, each = E.split_weight(a, k, b, u)
self.assertAlmostEqual(each, 1 + math.log(a / (k * u), b))
self.assertAlmostEqual(tot, k * (1 + math.log(a / (k * u), b)))
self.assertGreater(tot, E.log_weight(a, b, u))
self.assertEqual(E.split_weight(a, int(a / u) + 1, b, u)[0], 0.0) # every fake below unit: nothing
best, bk = E.best_split(a, b, u)
self.assertGreaterEqual(best, tot)
self.assertAlmostEqual(best, a / u) # b > e: best is k = a/unit, weight 1 each
def test_per_person_cap_and_unit_floor_below_amount_do_not_neutralise_splitting(self):
c = self.cfg()
b, u, a = c["euro_b"], c["euro_unit"], 1e6
single = E.log_weight(a, b, u)
for wc in (1.0, 3.0, single):
best, k = E.best_split(a, b, u, person_cap=wc)
self.assertGreater(best, min(single, wc))
self.assertGreater(E.best_split(a, b, 1000.0)[1], 1) # raised unit still splits
self.assertEqual(E.best_split(a, b, a)[1], 1) # unit = amount leaves nothing to split
def test_identity_cost_neutralises_only_at_a_large_share_of_the_budget(self):
c = self.cfg()
b, u, a = c["euro_b"], c["euro_unit"], 1e6
cost = E.min_neutralising_cost(a, b, u)
self.assertIsNotNone(cost)
self.assertEqual(E.best_split(a, b, u, identity_cost=cost)[1], 1)
self.assertGreater(E.best_split(a, b, u, identity_cost=cost / 2)[1], 1)
self.assertGreater(cost, a / 4)
self.assertAlmostEqual(cost / a, .5, delta=.01) # bisection value: just under half the budget
self.assertLess(cost, a / 2 + 1e-9)
def test_headline_numbers_are_pinned(self):
"""The figures REPORT.md quotes, under the placeholder defaults and the fixed seeds (40, STAKE_SEED)."""
W = E.WEIGHTS["equal"]
r = E.buyer_runs(1e6, E.ROLL["Patrons"], 40, W, (self.P,))
m, hw = E.mean_ci([x["s"] for x in r])
self.assertEqual(round(m, 3), .524)
self.assertEqual(round(hw, 3), .008)
self.assertEqual(sum(x["s"] > .5 for x in r) / 40, .85)
c = self.cfg()
w1 = E.log_weight(1e6, c["euro_b"], c["euro_unit"])
for k, want in ((2, 1.9), (10, 8.3), (100, 66.7)):
self.assertEqual(round(E.split_weight(1e6, k, c["euro_b"], c["euro_unit"])[0] / w1, 1), want)
for n in (40, 400): # the headline is a small-roll fact
self.assertLess(max(x["s"] for x in E.buyer_runs(1e6, n, 40, W, (self.P,))), .5)
def test_per_identity_cap_is_evaded_by_splitting(self):
c = self.cfg()
W = E.WEIGHTS["equal"]
rest = E.ROLL["Patrons"] * 1.0
single = E.stake_weight("Patrons", euros=1e6)
self.assertTrue(E.influence_cap(single, rest, W, self.P, (self.P,))["binds"])
k = 10
tot, wi = E.split_weight(1e6, k, c["euro_b"], c["euro_unit"])
self.assertFalse(E.influence_cap(wi, rest + tot - wi, W, self.P, (self.P,))["binds"])
self.assertGreaterEqual(E.council_share(tot, rest, W, self.P, (self.P,))[1], E.HALF)
def test_pareto_ladder_is_deterministic_and_above_floor(self):
a, b = E.pareto_ladder(50, random.Random(7)), E.pareto_ladder(50, random.Random(7))
self.assertEqual(a, b)
self.assertTrue(all(x >= E.PARETO_XM for x in a))
class StrategyTests(unittest.TestCase):
def test_deviation_never_makes_agent_worse_off(self):
rng = random.Random(3)