#!/usr/bin/env python3 """ Side-by-side e-basis comparison: MTP-delta vs direct stamp-delta. Same seed, same gains, same scenarios - only the e measurement differs. Prints one table per scenario with both columns so the difference is visible directly. Run: python3 compare_basis.py """ import os import random import sys sys.path.insert(0, os.path.dirname(__file__)) import mtp_sim as M class Cfg: def __init__(self, ebasis, seed, scenario="steady", blocks=8000, drop=0.9, ramp_x=10.0, attack_share=0.2, monotonic=False): self.window = 101 self.future = 60 self.kp = 50e6 self.ki = 10e6 self.kd = 16.7e6 self.alpha = 0.10 self.lp = 0.15 self.fill = 6600 self.ebasis = ebasis self.seed = seed self.scenario = scenario self.blocks = blocks self.drop = drop self.ramp_x = ramp_x self.attack_share = attack_share self.monotonic = monotonic self.flood_x = 50 self.flood_min = 30 self.stall_frac = 0.001 self.stall_days = 4 def run(cfg): rng = random.Random(cfg.seed) world = M.scenarios(cfg) chain = M.Chain(cfg) chain.add_block(0.0, False, seed=True) exit_t = M.flood_exit(cfg) for _ in range(cfg.blocks): h, a, mode = world.rates_at(chain.t) total = h + a if total <= 0: break dt = rng.expovariate(total / (M.T * chain.d)) arr = chain.t + dt nxt = world.next_boundary(chain.t) if nxt is not None and arr >= nxt: chain.advance_time(nxt) continue attacker = rng.random() < a / total chain.add_block(arr, attacker, mode=mode if attacker else "honest") ints = sorted(b[2] for b in chain.blocks if b[2] > 0) n = max(1, len(ints)) out = { "mean_int": sum(ints) / n, "med_int": ints[n // 2], "p90_int": ints[int(n * 0.9)], "iss_ratio": chain.issuance / (M.R_FULL * chain.t / M.T), "d_max": max(b[3] for b in chain.blocks), "d_final": chain.d, "att_reward": chain.attacker_reward, "honest_reward": chain.honest_reward, "max_blk_MB": chain.max_block_size / 1e6, "max_backlog_MB": chain.max_backlog / 1e6, } if exit_t is not None: post = [b for b in chain.blocks if b[0] >= exit_t and not b[1]] restore = None if post: good = 0 for b in post: if abs(b[2] - M.T) <= 0.5 * M.T: good += 1 else: good = 0 if good >= 3: restore = (b[0] - exit_t) / 3600 break out["restore_h"] = restore return out def row(label, a, b, fmt="{:.2f}", unit=""): def g(v): if v is None: return " -" return fmt.format(v) + unit print(f" {label:22s} {g(a):>10s} {g(b):>10s}") def compare(scenario, blocks, seed, drop=None, ramp_x=None, attack_share=None, monotonic=False): print(f"\n=== {scenario} (seed={seed}, blocks={blocks}, kp=50M ki=10M kd=16.7M" f"{', drop=' + str(drop) if drop is not None else ''}" f"{', ramp_x=' + str(ramp_x) if ramp_x is not None else ''}" f"{', share=' + str(attack_share) if attack_share is not None else ''}) ===") print(f" {'metric':22s} {'mtp':>10s} {'direct':>10s}") res = {} for basis in ("mtp", "direct"): cfg = Cfg(basis, seed, scenario, blocks, drop=drop if drop is not None else 0.9, ramp_x=ramp_x if ramp_x is not None else 10.0, attack_share=attack_share if attack_share is not None else 0.2, monotonic=monotonic) res[basis] = run(cfg) a, b = res["mtp"], res["direct"] row("mean interval", a["mean_int"], b["mean_int"], "{:.0f}", "s") row("median interval", a["med_int"], b["med_int"], "{:.0f}", "s") row("p90 interval", a["p90_int"], b["p90_int"], "{:.0f}", "s") row("issuance/parity", a["iss_ratio"], b["iss_ratio"], "{:.4f}") row("d max", a["d_max"], b["d_max"], "{:.2f}") row("d final", a["d_final"], b["d_final"], "{:.2f}") row("attacker reward", a["att_reward"], b["att_reward"], "{:.2f}") if attack_share is not None: fa = a["att_reward"] + a["honest_reward"] fb = b["att_reward"] + b["honest_reward"] row("att fair share", attack_share * fa, attack_share * fb, "{:.2f}") row("att gain", a["att_reward"] - attack_share * fa, b["att_reward"] - attack_share * fb, "{:+.2f}") row("max block", a["max_blk_MB"], b["max_blk_MB"], "{:.1f}", "MB") row("max backlog", a["max_backlog_MB"], b["max_backlog_MB"], "{:.1f}", "MB") if "restore_h" in a: row("cadence restore", a["restore_h"], b["restore_h"], "{:.1f}", "h") def main(): seed = 7 compare("steady", 16000, seed) compare("flood", 8000, seed) compare("collapse", 8000, seed, drop=0.9) compare("collapse99", 8000, seed, drop=0.99) compare("ramp_fast", 9000, seed, ramp_x=10) compare("ramp100", 9000, seed, ramp_x=100) compare("ramp1000", 9000, seed, ramp_x=1000) compare("ramp1e6", 9000, seed, ramp_x=1e6) compare("future_minority", 8000, seed, attack_share=0.2) compare("future_majority", 8000, seed, attack_share=0.6) compare("past_minority", 8000, seed, attack_share=0.2) compare("past_majority", 8000, seed, attack_share=0.6) compare("stall", 6000, seed) if __name__ == "__main__": main()