| """ |
| tuner.py — turn the Clutch demo into a TOOL: tune the gate on the USER's own data. |
| |
| The visitor pastes/uploads any 1-D time series (latency metric, sensor stream, price |
| feed, error signal...). We run the real closed-loop drift substrate on it: |
| cheap = extrapolate the cached linear model (O(1)) |
| costly = least-squares refit on the last `window` (O(window)) |
| error = normalized residual of the last prediction |
| Then we SWEEP both gate families over a parameter grid, plot the honest |
| accuracy-vs-compute Pareto frontier, and pick the cheapest config whose MAE is within |
| `tol` of refit-every-step. Output: a copy-paste Clutch(...) snippet + $ savings. |
| |
| All counterfactuals are valid because the loop is closed (refitting changes future |
| errors) — this is the same honest accounting as the benchmark, on the user's data. |
| """ |
|
|
| import io |
| import re |
| import numpy as np |
| from drift import run_drift |
|
|
| MAX_POINTS = 20_000 |
|
|
|
|
| |
| def parse_series(text=None, file_obj=None): |
| """Extract a 1-D float series from pasted text or an uploaded CSV/TXT. |
| Multi-column CSV: uses the LAST numeric column (usually the value column). |
| Returns (y, message).""" |
| raw = "" |
| if file_obj is not None: |
| path = file_obj if isinstance(file_obj, str) else getattr(file_obj, "name", None) |
| if path: |
| with open(path, "r", errors="ignore") as f: |
| raw = f.read() |
| elif text: |
| raw = text |
| if not raw.strip(): |
| return None, "No data provided." |
|
|
| rows = [] |
| for line in raw.strip().splitlines(): |
| nums = re.findall(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?", line) |
| if nums: |
| rows.append([float(x) for x in nums]) |
| if not rows: |
| return None, "Could not find any numbers in the input." |
|
|
| ncol = max(len(r) for r in rows) |
| if ncol == 1: |
| y = np.array([r[0] for r in rows if len(r) == 1], dtype=float) |
| note = "" |
| else: |
| |
| full = [r for r in rows if len(r) == ncol] |
| y = np.array([r[-1] for r in full], dtype=float) |
| note = f" (took last of {ncol} numeric columns)" |
| y = y[np.isfinite(y)] |
| if len(y) < 60: |
| return None, f"Found only {len(y)} points — need at least 60 for a meaningful sweep." |
| if len(y) > MAX_POINTS: |
| y = y[:MAX_POINTS] |
| note += f"; truncated to first {MAX_POINTS:,} points" |
| return y, f"Loaded {len(y):,} points{note}." |
|
|
|
|
| |
| MAG_GRID = [dict(gain=g, leak=l, trip_mag=t) |
| for g in (2.0, 4.0, 6.0, 8.0) |
| for l in (0.2, 0.5) |
| for t in (0.25, 0.5, 1.0, 2.0, 3.0, 4.0, 6.0, 8.0)] |
| ACC_GRID = [dict(trip_acc=t, refractory=r) |
| for t in (0.2, 0.4, 0.8, 1.2, 2.0) |
| for r in (0, 3, 6)] |
|
|
|
|
| def sweep(y, window=25): |
| """Run baselines + full grid. Returns dict with baselines and per-config rows.""" |
| cps = set() |
| ref = run_drift(y, cps, "ALWAYS_REFIT", window=window) |
| never = run_drift(y, cps, "NEVER_REFIT", window=window) |
| rows = [] |
| for gp in MAG_GRID: |
| r = run_drift(y, cps, "CLUTCH_MAG", window=window, gate_params=gp) |
| rows.append(dict(gate="MagnitudeGate", params=gp, mae=r["mae"], |
| refits=r["refits"], samples=r["refit_samples"])) |
| for gp in ACC_GRID: |
| r = run_drift(y, cps, "CLUTCH_ACC", window=window, gate_params=gp) |
| rows.append(dict(gate="AcceleratorGate", params=gp, mae=r["mae"], |
| refits=r["refits"], samples=r["refit_samples"])) |
| return dict(ref=ref, never=never, rows=rows, window=window, n=len(y)) |
|
|
|
|
| def pick_best(sw, tol=0.10): |
| """Cheapest config with MAE <= (1+tol) * refit-every-step MAE. |
| Returns (best_or_None, fallback, limit). fallback = Pareto point with the |
| smallest MAE increase that still saves >= 30% compute (honest 'closest option').""" |
| limit = sw["ref"]["mae"] * (1.0 + tol) |
| ok = [r for r in sw["rows"] if r["mae"] <= limit] |
| best = min(ok, key=lambda r: (r["samples"], r["mae"])) if ok else None |
| base = sw["ref"]["refit_samples"] or 1 |
| savers = [r for r in pareto_front(sw["rows"]) if r["samples"] <= 0.7 * base] |
| fallback = min(savers, key=lambda r: r["mae"]) if savers else None |
| return best, fallback, limit |
|
|
|
|
| def pareto_front(rows): |
| """Non-dominated set in (samples, mae), sorted by samples.""" |
| pts = sorted(rows, key=lambda r: (r["samples"], r["mae"])) |
| front, best_mae = [], float("inf") |
| for r in pts: |
| if r["mae"] < best_mae - 1e-12: |
| front.append(r) |
| best_mae = r["mae"] |
| return front |
|
|
|
|
| |
| def code_snippet(best, window): |
| p = best["params"] |
| if best["gate"] == "MagnitudeGate": |
| gate = (f"MagnitudeGate(gain={p['gain']}, leak={p['leak']}, " |
| f"trip={p['trip_mag']})") |
| else: |
| gate = f"AcceleratorGate(trip={p['trip_acc']}, refractory={p['refractory']})" |
| return f"""# tuned on YOUR data — cheapest gate within tolerance of refit-every-step |
| from clutch import Clutch, MagnitudeGate, AcceleratorGate |
| |
| clutch = Clutch({gate}) |
| # refit window used during tuning: {window} |
| # supply your three callbacks: |
| # cheap_step(state) -> action from the cached model/plan (O(1)) |
| # expensive_plan(state) -> (action, calm_bool) # your costly call |
| # error_signal(state) -> scalar >= 0, e.g. |prediction - truth| / scale |
| action, mode = clutch.step(state, cheap_step, expensive_plan, error_signal)""" |
|
|
|
|
| def report(sw, best, fallback, limit, tol, cost_per_call): |
| ref, never = sw["ref"], sw["never"] |
| win = best or fallback |
| lines = [f"#### Result on your {sw['n']:,}-point series (window {sw['window']})\n"] |
| lines.append("| strategy | MAE | expensive calls | training samples |") |
| lines.append("|---|---:|---:|---:|") |
| lines.append(f"| refit every step | {ref['mae']:.4g} | {ref['refits']:,} | {ref['refit_samples']:,} |") |
| if win: |
| tag = "tuned clutch" if best else "closest clutch (outside tolerance)" |
| lines.append(f"| **{tag}** | **{win['mae']:.4g}** | **{win['refits']:,}** | **{win['samples']:,}** |") |
| lines.append(f"| never refit | {never['mae']:.4g} | {never['refits']} | {never['refit_samples']:,} |") |
| lines.append("") |
| if best is None: |
| lines.append(f"⚠️ **No gate config stayed within {tol*100:.0f}% of the refit-every-step MAE " |
| f"(limit {limit:.4g}).** Honest verdict: on this series the cheap extrapolation " |
| "itself loses accuracy between refits, so gating is not free here.") |
| if fallback: |
| inc = (fallback["mae"] / ref["mae"] - 1) * 100 |
| frac = fallback["samples"] / (ref["refit_samples"] or 1) * 100 |
| lines.append(f"\nClosest trade-off on the Pareto frontier: `{fallback['gate']}` " |
| f"{fallback['params']} — **{frac:.1f}% of the compute for +{inc:.1f}% MAE**. " |
| "Take it only if that accuracy hit is acceptable; otherwise refit every step.") |
| return "\n".join(lines), win |
| frac = best["samples"] / ref["refit_samples"] if ref["refit_samples"] else 1.0 |
| saved_calls = ref["refits"] - best["refits"] |
| lines.append(f"**Winner: `{best['gate']}` {best['params']}** — matches refit-every-step accuracy " |
| f"(within {tol*100:.0f}%) using **{frac*100:.1f}% of the training compute** " |
| f"and **{best['refits']:,} expensive calls instead of {ref['refits']:,}**.") |
| if cost_per_call and cost_per_call > 0: |
| lines.append(f"\n💰 At **${cost_per_call:.4g} per expensive call** (e.g. an LLM re-plan), " |
| f"this trace costs **${ref['refits']*cost_per_call:,.2f}** always-refit vs " |
| f"**${best['refits']*cost_per_call:,.2f}** with the clutch — " |
| f"**${saved_calls*cost_per_call:,.2f} saved ({(1-best['refits']/ref['refits'])*100:.1f}%)** " |
| f"on this trace alone.") |
| return "\n".join(lines), win |
|
|
|
|
| |
| def preset_series(name, seed=0): |
| rng = np.random.default_rng(seed) |
| t = np.arange(900, dtype=float) |
| if name.startswith("Server latency"): |
| y = 120 + 8 * np.sin(t / 40) + rng.normal(0, 6, len(t)) |
| for s in (200, 480, 700): |
| y[s:s + 60] += np.linspace(0, 140, 60) * rng.uniform(0.6, 1.2) |
| return y |
| if name.startswith("Sensor with"): |
| y = np.cumsum(rng.normal(0, 0.05, len(t))) |
| for s in (150, 400, 650, 800): |
| y[s:] += rng.uniform(-3, 3) |
| return y + rng.normal(0, 0.15, len(t)) |
| |
| vol = np.where((t > 300) & (t < 500), 0.9, 0.25) |
| y = 100 + np.cumsum(rng.normal(0.02, 1, len(t)) * vol) |
| return y |
|
|