compare_basis.py raw
1 #!/usr/bin/env python3
2 """
3 Side-by-side e-basis comparison: MTP-delta vs direct stamp-delta.
4
5 Same seed, same gains, same scenarios - only the e measurement differs.
6 Prints one table per scenario with both columns so the difference is
7 visible directly.
8
9 Run: python3 compare_basis.py
10 """
11 import os
12 import random
13 import sys
14
15 sys.path.insert(0, os.path.dirname(__file__))
16 import mtp_sim as M
17
18
19 class Cfg:
20 def __init__(self, ebasis, seed, scenario="steady", blocks=8000,
21 drop=0.9, ramp_x=10.0, attack_share=0.2, monotonic=False):
22 self.window = 101
23 self.future = 60
24 self.kp = 50e6
25 self.ki = 10e6
26 self.kd = 16.7e6
27 self.alpha = 0.10
28 self.lp = 0.15
29 self.fill = 6600
30 self.ebasis = ebasis
31 self.seed = seed
32 self.scenario = scenario
33 self.blocks = blocks
34 self.drop = drop
35 self.ramp_x = ramp_x
36 self.attack_share = attack_share
37 self.monotonic = monotonic
38 self.flood_x = 50
39 self.flood_min = 30
40 self.stall_frac = 0.001
41 self.stall_days = 4
42
43
44 def run(cfg):
45 rng = random.Random(cfg.seed)
46 world = M.scenarios(cfg)
47 chain = M.Chain(cfg)
48 chain.add_block(0.0, False, seed=True)
49 exit_t = M.flood_exit(cfg)
50 for _ in range(cfg.blocks):
51 h, a, mode = world.rates_at(chain.t)
52 total = h + a
53 if total <= 0:
54 break
55 dt = rng.expovariate(total / (M.T * chain.d))
56 arr = chain.t + dt
57 nxt = world.next_boundary(chain.t)
58 if nxt is not None and arr >= nxt:
59 chain.advance_time(nxt)
60 continue
61 attacker = rng.random() < a / total
62 chain.add_block(arr, attacker, mode=mode if attacker else "honest")
63
64 ints = sorted(b[2] for b in chain.blocks if b[2] > 0)
65 n = max(1, len(ints))
66 out = {
67 "mean_int": sum(ints) / n,
68 "med_int": ints[n // 2],
69 "p90_int": ints[int(n * 0.9)],
70 "iss_ratio": chain.issuance / (M.R_FULL * chain.t / M.T),
71 "d_max": max(b[3] for b in chain.blocks),
72 "d_final": chain.d,
73 "att_reward": chain.attacker_reward,
74 "honest_reward": chain.honest_reward,
75 "max_blk_MB": chain.max_block_size / 1e6,
76 "max_backlog_MB": chain.max_backlog / 1e6,
77 }
78 if exit_t is not None:
79 post = [b for b in chain.blocks if b[0] >= exit_t and not b[1]]
80 restore = None
81 if post:
82 good = 0
83 for b in post:
84 if abs(b[2] - M.T) <= 0.5 * M.T:
85 good += 1
86 else:
87 good = 0
88 if good >= 3:
89 restore = (b[0] - exit_t) / 3600
90 break
91 out["restore_h"] = restore
92 return out
93
94
95 def row(label, a, b, fmt="{:.2f}", unit=""):
96 def g(v):
97 if v is None:
98 return " -"
99 return fmt.format(v) + unit
100 print(f" {label:22s} {g(a):>10s} {g(b):>10s}")
101
102
103 def compare(scenario, blocks, seed, drop=None, ramp_x=None, attack_share=None,
104 monotonic=False):
105 print(f"\n=== {scenario} (seed={seed}, blocks={blocks}, kp=50M ki=10M kd=16.7M"
106 f"{', drop=' + str(drop) if drop is not None else ''}"
107 f"{', ramp_x=' + str(ramp_x) if ramp_x is not None else ''}"
108 f"{', share=' + str(attack_share) if attack_share is not None else ''}) ===")
109 print(f" {'metric':22s} {'mtp':>10s} {'direct':>10s}")
110 res = {}
111 for basis in ("mtp", "direct"):
112 cfg = Cfg(basis, seed, scenario, blocks,
113 drop=drop if drop is not None else 0.9,
114 ramp_x=ramp_x if ramp_x is not None else 10.0,
115 attack_share=attack_share if attack_share is not None else 0.2,
116 monotonic=monotonic)
117 res[basis] = run(cfg)
118 a, b = res["mtp"], res["direct"]
119 row("mean interval", a["mean_int"], b["mean_int"], "{:.0f}", "s")
120 row("median interval", a["med_int"], b["med_int"], "{:.0f}", "s")
121 row("p90 interval", a["p90_int"], b["p90_int"], "{:.0f}", "s")
122 row("issuance/parity", a["iss_ratio"], b["iss_ratio"], "{:.4f}")
123 row("d max", a["d_max"], b["d_max"], "{:.2f}")
124 row("d final", a["d_final"], b["d_final"], "{:.2f}")
125 row("attacker reward", a["att_reward"], b["att_reward"], "{:.2f}")
126 if attack_share is not None:
127 fa = a["att_reward"] + a["honest_reward"]
128 fb = b["att_reward"] + b["honest_reward"]
129 row("att fair share", attack_share * fa, attack_share * fb, "{:.2f}")
130 row("att gain", a["att_reward"] - attack_share * fa,
131 b["att_reward"] - attack_share * fb, "{:+.2f}")
132 row("max block", a["max_blk_MB"], b["max_blk_MB"], "{:.1f}", "MB")
133 row("max backlog", a["max_backlog_MB"], b["max_backlog_MB"], "{:.1f}", "MB")
134 if "restore_h" in a:
135 row("cadence restore", a["restore_h"], b["restore_h"], "{:.1f}", "h")
136
137
138 def main():
139 seed = 7
140 compare("steady", 16000, seed)
141 compare("flood", 8000, seed)
142 compare("collapse", 8000, seed, drop=0.9)
143 compare("collapse99", 8000, seed, drop=0.99)
144 compare("ramp_fast", 9000, seed, ramp_x=10)
145 compare("ramp100", 9000, seed, ramp_x=100)
146 compare("ramp1000", 9000, seed, ramp_x=1000)
147 compare("ramp1e6", 9000, seed, ramp_x=1e6)
148 compare("future_minority", 8000, seed, attack_share=0.2)
149 compare("future_majority", 8000, seed, attack_share=0.6)
150 compare("past_minority", 8000, seed, attack_share=0.2)
151 compare("past_majority", 8000, seed, attack_share=0.6)
152 compare("stall", 6000, seed)
153
154
155 if __name__ == "__main__":
156 main()
157