#!/usr/bin/env python3 """ I vs PI comparison - clean, controlled, same seeds. Naming corrected from the swapped 2021 lineage: P (proportional) = gain on the instantaneous error (kp) I (integral) = gain on the EMA accumulator (ki) D (derivative) = gain on the band-passed error difference (kd) The claim under test: the proportional kick (P) added to the integral accumulator (I) tracks hashpower changes faster. (Previously reported as "P vs PI" under the swapped labels - the physics is unchanged, the labels are corrected.) Four configs, same seeds, D=60s delay floor, monotonic stamps, direct e: sym-I kp=0, ki=10M (integral only) sym-PI kp=10M, ki=10M (symmetric P+I) asym-I kp=0, ki_up=5M, ki_down=50M (asymmetric integral only) asym-PI kp=10M, ki_up=5M, ki_down=50M (asymmetric P+I - final design) Metrics: steady: mean interval, d wander (log-std of d - oscillation amplitude) step 3x at 20d: t50 (hours to reach d=2), overshoot (max d) collapse -90%: t50-down (hours for d to fall halfway to 0.1) Run: python3 pi_compare.py """ import math import os import random import sys sys.path.insert(0, os.path.dirname(__file__)) import mtp_sim as M class Cfg: def __init__(self, kp, ki_up, ki_down, seed=7, blocks=9000): self.window = 101 self.future = 60 self.kp = kp self.ki = ki_up self.kd = 0 self.ki_up = ki_up self.ki_down = ki_down self.alpha = 0.05 self.lp = 0.15 self.fill = 6600 self.ebasis = "direct" self.monotonic = True self.delay = 60.0 self.att_hw = 1.0 self.prop_base = 0.0 self.bandwidth = 100e6 self.honest_miners = 1 self.scenario = "steady" self.seed = seed self.blocks = blocks self.drop = 0.9 self.ramp_x = 10.0 self.flood_x = 50 self.flood_min = 30 self.stall_frac = 0.001 self.stall_days = 4 self.attack_share = 0.6 def simulate(cfg, step_t=None, collapse_t=None): """Run the chain. Returns (chain, step_t50, step_overshoot, coll_t50).""" rng = random.Random(cfg.seed) world = M.World() world.const(0, 1.0) if step_t is not None: world.const(step_t, 3.0) if collapse_t is not None: world.const(collapse_t, 1.0 - cfg.drop) chain = M.Chain(cfg) chain.add_block(0.0, False, seed=True) step_t50 = None step_over = 1.0 coll_t50 = None coll_target = 0.5 + (1.0 - cfg.drop) / 2.0 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 hw = chain.att_hw if (a > 0 and rng.random() < a / total) else 1.0 attacker = hw != 1.0 min_arr = chain.last_block_t + chain.delay * hw if arr < min_arr: arr = min_arr if nxt is not None and arr >= nxt: chain.advance_time(nxt) continue chain.add_block(arr, attacker) if step_t is not None and chain.t > step_t: step_over = max(step_over, chain.d) if step_t50 is None and chain.d >= 2.0: step_t50 = (chain.t - step_t) / 3600 if collapse_t is not None and chain.t > collapse_t and coll_t50 is None: if chain.d <= coll_target: coll_t50 = (chain.t - collapse_t) / 3600 return chain, step_t50, step_over, coll_t50 def steady_metrics(cfg): chain, _, _, _ = simulate(cfg) ints = sorted(b[2] for b in chain.blocks if b[2] > 0) n = max(1, len(ints)) dlogs = [math.log(b[3]) for b in chain.blocks[cfg.blocks // 3:]] mean = sum(dlogs) / len(dlogs) var = sum((x - mean) ** 2 for x in dlogs) / len(dlogs) return sum(ints) / n, math.sqrt(var), chain.d def report(name, kp, ki_up, ki_down, seeds=(7, 8)): rows = [] for seed in seeds: cfg = Cfg(kp, ki_up, ki_down, seed=seed) mean_int, wander, d_fin = steady_metrics(cfg) cfg2 = Cfg(kp, ki_up, ki_down, seed=seed) _, t50, over, _ = simulate(cfg2, step_t=20 * 86400) cfg3 = Cfg(kp, ki_up, ki_down, seed=seed, blocks=12000) _, _, _, coll = simulate(cfg3, collapse_t=100 * M.T) rows.append((seed, mean_int, wander, d_fin, t50, over, coll)) print(f"{name:10s} | mean_int wander d_fin | step_t50 overshoot | coll_t50") for seed, mean_int, wander, d_fin, t50, over, coll in rows: t50s = f"{t50:7.1f}h" if t50 else " -" overs = f"{over:9.2f}x" if over else " -" colls = f"{coll:7.1f}h" if coll else " -" print(f" seed {seed} | {mean_int:7.0f}s {wander:7.3f} {d_fin:6.2f} | " f"{t50s} {overs} | {colls}") print() def main(): print("I vs PI, same seeds, D=60s delay floor, monotonic, direct e") print("(labels corrected: P = instantaneous error gain, I = EMA gain)\n") report("sym-I", 0, 10e6, 10e6) report("sym-PI", 10e6, 10e6, 10e6) report("asym-I", 0, 5e6, 50e6) report("asym-PI", 10e6, 5e6, 50e6) if __name__ == "__main__": main()