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