"""Summarize one run's probe.csv (and routerping.csv, if present) between two HH:MM:SS times. Same phase rule, idle baseline and percentile as video #1's analyze.py (so medians and p95 match it exactly), plus what video #2's plan needs: the worst packet, packets over +100 ms, and the router ping per phase. Usage: python analyze_v2.py START END [probe.csv] [routerping.csv] """ import csv, statistics, sys start, end = sys.argv[1], sys.argv[2] probe = sys.argv[3] if len(sys.argv) > 3 else 'probe.csv' router = sys.argv[4] if len(sys.argv) > 4 else None def secs(ts): return int(ts[:2]) * 3600 + int(ts[3:5]) * 60 + float(ts[6:]) rows = [r for r in csv.DictReader(open(probe)) if start <= r['ts'][:8] <= end] for r in rows: for k in ('udp_ms', 'icmp_ms', 'tx_mbps', 'rx_mbps'): r[k] = float(r[k]) def phase(r): # analyze.py's rule if r['tx_mbps'] > 100 and r['tx_mbps'] > r['rx_mbps']: return 'upload' if r['rx_mbps'] > 100 and r['rx_mbps'] > r['tx_mbps']: return 'download' if r['tx_mbps'] < 5 and r['rx_mbps'] < 5: return 'idle' return 'other' def pct(v, p): # analyze.py's percentile v = sorted(v); return v[min(len(v) - 1, int(round(p / 100 * (len(v) - 1))))] times = [secs(r['ts']) for r in rows] gap = max((b - a for a, b in zip(times, times[1:])), default=0) print(f"samples {len(rows)} max gap {gap:.2f}s") idle_udp = [r['udp_ms'] for r in rows if phase(r) == 'idle' and r['udp_ms'] > 0] base_udp = statistics.median(idle_udp) if idle_udp else float('nan') idle_icmp = [r['icmp_ms'] for r in rows if phase(r) == 'idle' and r['icmp_ms'] > 0] base_icmp = statistics.median(idle_icmp) if idle_icmp else float('nan') print(f"idle baseline: udp {base_udp:.1f}ms (n={len(idle_udp)}) icmp {base_icmp:.1f}ms") gw = [] if router: gw = [(secs(r['ts']), float(r['gw_ms'])) for r in csv.DictReader(open(router)) if start <= r['ts'][:8] <= end] targets = {r['target'] for r in csv.DictReader(open(router))} gidle = [ms for t, ms in gw if ms >= 0 and any(abs(t - pt) < 0.25 and phase(pr) == 'idle' for pt, pr in zip(times, rows))] print(f"router ping target {', '.join(sorted(targets))}: {len(gw)} samples, idle median " f"{statistics.median(gidle) if gidle else float('nan'):.1f}ms") for ph in ('download', 'upload'): sel = [r for r in rows if phase(r) == ph] u = [r['udp_ms'] for r in sel if r['udp_ms'] > 0]; lost_u = sum(1 for r in sel if r['udp_ms'] < 0) if not u: print(f"{ph:8s} n={len(sel)} (no valid samples)") continue mb = statistics.median([r['tx_mbps' if ph == 'upload' else 'rx_mbps'] for r in sel]) i = [r['icmp_ms'] for r in sel if r['icmp_ms'] >= 0] over = sum(1 for x in u if x - base_udp > 100) line = (f"{ph:8s} n={len(sel)} ~{mb:.0f} Mbps | UDP added: median +{statistics.median(u) - base_udp:.0f}ms " f"p95 +{pct(u, 95) - base_udp:.0f}ms worst +{max(u) - base_udp:.0f}ms over+100 {over}/{len(u)} timeouts {lost_u}") if i: line += f" | ICMP added: median +{statistics.median(i) - base_icmp:.0f}ms p95 +{pct(i, 95) - base_icmp:.0f}ms" if gw: st = {round(secs(r['ts']), 1) for r in sel} g = [ms for t, ms in gw if ms >= 0 and any(abs(t - s) < 0.25 for s in st)] lost_g = sum(1 for t, ms in gw if ms < 0 and any(abs(t - s) < 0.25 for s in st)) if g: line += f" | router ping: median {statistics.median(g):.0f}ms p95 {pct(g, 95):.0f}ms worst {max(g):.0f}ms lost {lost_g}" print(line)