""" Operon Feedback Loop Homeostasis -- Interactive Gradio Demo =========================================================== Simulate a NegativeFeedbackLoop controlling a value toward a setpoint. Configure gain, damping, and disturbances to watch convergence, oscillation, or overdamping in real time. Run locally: pip install gradio python space-feedback/app.py Deploy to HuggingFace Spaces: Copy this directory to a new HF Space with sdk=gradio. """ import sys from pathlib import Path import gradio as gr # Allow importing operon_ai from the repo root when running locally _repo_root = Path(__file__).resolve().parent.parent if str(_repo_root) not in sys.path: sys.path.insert(0, str(_repo_root)) from operon_ai import NegativeFeedbackLoop # ── Presets ──────────────────────────────────────────────────────────────── PRESETS: dict[str, dict] = { "(custom)": { "description": "Configure your own feedback loop parameters.", "setpoint": 0.0, "initial": 10.0, "gain": 0.5, "damping": 0.1, "iterations": 30, "disturbance_step": 0, "disturbance_magnitude": 0.0, }, "Temperature control": { "description": "Smooth cooling from 85°F toward 72°F setpoint — classic thermostat behavior.", "setpoint": 72.0, "initial": 85.0, "gain": 0.3, "damping": 0.05, "iterations": 30, "disturbance_step": 0, "disturbance_magnitude": 0.0, }, "Oscillating convergence": { "description": "High gain causes overshooting around the setpoint before settling.", "setpoint": 0.0, "initial": 10.0, "gain": 0.8, "damping": 0.0, "iterations": 40, "disturbance_step": 0, "disturbance_magnitude": 0.0, }, "Overdamped": { "description": "Heavy damping — slow, stable approach with no overshoot.", "setpoint": 50.0, "initial": 0.0, "gain": 0.2, "damping": 0.3, "iterations": 50, "disturbance_step": 0, "disturbance_magnitude": 0.0, }, "Underdamped": { "description": "Light damping + high gain — fast oscillations that ring before settling.", "setpoint": 50.0, "initial": 0.0, "gain": 0.9, "damping": 0.02, "iterations": 40, "disturbance_step": 0, "disturbance_magnitude": 0.0, }, "Disturbance rejection": { "description": "System at setpoint gets a -30 disturbance at step 10 — watch recovery.", "setpoint": 100.0, "initial": 100.0, "gain": 0.3, "damping": 0.05, "iterations": 40, "disturbance_step": 10, "disturbance_magnitude": -30.0, }, } def _load_preset( name: str, ) -> tuple[float, float, float, float, int, int, float]: """Return slider values for a preset.""" p = PRESETS.get(name, PRESETS["(custom)"]) return ( p["setpoint"], p["initial"], p["gain"], p["damping"], p["iterations"], p["disturbance_step"], p["disturbance_magnitude"], ) # ── Core simulation ─────────────────────────────────────────────────────── def run_feedback( preset_name: str, setpoint: float, initial: float, gain: float, damping: float, iterations: int, disturbance_step: int, disturbance_magnitude: float, ) -> tuple[str, str, str]: """Run the feedback loop simulation. Returns (convergence_banner_html, timeline_md, analysis_md). """ loop = NegativeFeedbackLoop( setpoint=setpoint, gain=gain, damping=damping, silent=True, ) current = initial rows: list[dict] = [] initial_error = abs(initial - setpoint) for step in range(1, int(iterations) + 1): note = "" # Apply disturbance if disturbance_step > 0 and step == int(disturbance_step): current += disturbance_magnitude note = f"Disturbance: {disturbance_magnitude:+.1f}" error_before = current - setpoint correction = loop.apply(current) current = correction # apply() returns the corrected value error_after = current - setpoint rows.append({ "step": step, "value": current, "error": error_after, "correction": current - (error_before + setpoint), "note": note, }) # ── Convergence analysis ────────────────────────────────────────── final_error = abs(rows[-1]["error"]) if rows else initial_error threshold = max(initial_error * 0.01, 0.01) # 1% of initial distance converged = final_error <= threshold converge_step = None for r in rows: if abs(r["error"]) <= threshold: converge_step = r["step"] break if converged: color, label = "#22c55e", "CONVERGED" detail = f"Final error: {final_error:.4f} (within 1% of initial distance {initial_error:.2f})" if converge_step: detail += f" — converged at step {converge_step}" else: color, label = "#ef4444", "NOT CONVERGED" detail = f"Final error: {final_error:.4f} (threshold: {threshold:.4f})" banner = ( f'