coredipper commited on
Commit
8823e12
·
verified ·
1 Parent(s): c62afdb

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. README.md +21 -5
  2. __pycache__/app.cpython-314.pyc +0 -0
  3. app.py +292 -0
  4. requirements.txt +2 -0
README.md CHANGED
@@ -1,12 +1,28 @@
1
  ---
2
- title: Operon Feedback
3
- emoji: 🚀
4
  colorFrom: yellow
5
- colorTo: gray
6
  sdk: gradio
7
- sdk_version: 6.5.1
8
  app_file: app.py
9
  pinned: false
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Operon Feedback Loop Homeostasis
3
+ emoji: ⚖️
4
  colorFrom: yellow
5
+ colorTo: green
6
  sdk: gradio
7
+ sdk_version: "6.5.1"
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
+ short_description: Negative feedback loop homeostasis simulation
12
  ---
13
 
14
+ # ⚖️ Feedback Loop Homeostasis
15
+
16
+ Simulate a **NegativeFeedbackLoop** controlling a value toward a setpoint. Configure gain, damping, and disturbances to watch convergence, oscillation, or overdamping in real time.
17
+
18
+ ## Features
19
+
20
+ - **6 presets**: Temperature control, oscillating convergence, overdamped, underdamped, disturbance rejection
21
+ - **Tunable parameters**: Setpoint, gain, damping, iterations, disturbance injection
22
+ - **Convergence analysis**: Steps to within 1% of setpoint, loop statistics
23
+
24
+ ## How It Works
25
+
26
+ The `NegativeFeedbackLoop` computes a correction at each step based on the error (distance from setpoint), scaled by gain and damped to prevent oscillation. This mirrors biological homeostasis — thermostats, blood sugar regulation, and neural feedback.
27
+
28
+ [GitHub](https://github.com/coredipper/operon) | [PyPI](https://pypi.org/project/operon-ai/)
__pycache__/app.cpython-314.pyc ADDED
Binary file (12.1 kB). View file
 
app.py ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Operon Feedback Loop Homeostasis -- Interactive Gradio Demo
3
+ ===========================================================
4
+
5
+ Simulate a NegativeFeedbackLoop controlling a value toward a setpoint.
6
+ Configure gain, damping, and disturbances to watch convergence, oscillation,
7
+ or overdamping in real time.
8
+
9
+ Run locally:
10
+ pip install gradio
11
+ python space-feedback/app.py
12
+
13
+ Deploy to HuggingFace Spaces:
14
+ Copy this directory to a new HF Space with sdk=gradio.
15
+ """
16
+
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ import gradio as gr
21
+
22
+ # Allow importing operon_ai from the repo root when running locally
23
+ _repo_root = Path(__file__).resolve().parent.parent
24
+ if str(_repo_root) not in sys.path:
25
+ sys.path.insert(0, str(_repo_root))
26
+
27
+ from operon_ai import NegativeFeedbackLoop
28
+
29
+ # ── Presets ────────────────────────────────────────────────────────────────
30
+
31
+ PRESETS: dict[str, dict] = {
32
+ "(custom)": {
33
+ "description": "Configure your own feedback loop parameters.",
34
+ "setpoint": 0.0,
35
+ "initial": 10.0,
36
+ "gain": 0.5,
37
+ "damping": 0.1,
38
+ "iterations": 30,
39
+ "disturbance_step": 0,
40
+ "disturbance_magnitude": 0.0,
41
+ },
42
+ "Temperature control": {
43
+ "description": "Smooth cooling from 85°F toward 72°F setpoint — classic thermostat behavior.",
44
+ "setpoint": 72.0,
45
+ "initial": 85.0,
46
+ "gain": 0.3,
47
+ "damping": 0.05,
48
+ "iterations": 30,
49
+ "disturbance_step": 0,
50
+ "disturbance_magnitude": 0.0,
51
+ },
52
+ "Oscillating convergence": {
53
+ "description": "High gain causes overshooting around the setpoint before settling.",
54
+ "setpoint": 0.0,
55
+ "initial": 10.0,
56
+ "gain": 0.8,
57
+ "damping": 0.0,
58
+ "iterations": 40,
59
+ "disturbance_step": 0,
60
+ "disturbance_magnitude": 0.0,
61
+ },
62
+ "Overdamped": {
63
+ "description": "Heavy damping — slow, stable approach with no overshoot.",
64
+ "setpoint": 50.0,
65
+ "initial": 0.0,
66
+ "gain": 0.2,
67
+ "damping": 0.3,
68
+ "iterations": 50,
69
+ "disturbance_step": 0,
70
+ "disturbance_magnitude": 0.0,
71
+ },
72
+ "Underdamped": {
73
+ "description": "Light damping + high gain — fast oscillations that ring before settling.",
74
+ "setpoint": 50.0,
75
+ "initial": 0.0,
76
+ "gain": 0.9,
77
+ "damping": 0.02,
78
+ "iterations": 40,
79
+ "disturbance_step": 0,
80
+ "disturbance_magnitude": 0.0,
81
+ },
82
+ "Disturbance rejection": {
83
+ "description": "System at setpoint gets a -30 disturbance at step 10 — watch recovery.",
84
+ "setpoint": 100.0,
85
+ "initial": 100.0,
86
+ "gain": 0.3,
87
+ "damping": 0.05,
88
+ "iterations": 40,
89
+ "disturbance_step": 10,
90
+ "disturbance_magnitude": -30.0,
91
+ },
92
+ }
93
+
94
+
95
+ def _load_preset(
96
+ name: str,
97
+ ) -> tuple[float, float, float, float, int, int, float]:
98
+ """Return slider values for a preset."""
99
+ p = PRESETS.get(name, PRESETS["(custom)"])
100
+ return (
101
+ p["setpoint"],
102
+ p["initial"],
103
+ p["gain"],
104
+ p["damping"],
105
+ p["iterations"],
106
+ p["disturbance_step"],
107
+ p["disturbance_magnitude"],
108
+ )
109
+
110
+
111
+ # ── Core simulation ───────────────────────────────────────────────────────
112
+
113
+
114
+ def run_feedback(
115
+ preset_name: str,
116
+ setpoint: float,
117
+ initial: float,
118
+ gain: float,
119
+ damping: float,
120
+ iterations: int,
121
+ disturbance_step: int,
122
+ disturbance_magnitude: float,
123
+ ) -> tuple[str, str, str]:
124
+ """Run the feedback loop simulation.
125
+
126
+ Returns (convergence_banner_html, timeline_md, analysis_md).
127
+ """
128
+ loop = NegativeFeedbackLoop(
129
+ setpoint=setpoint,
130
+ gain=gain,
131
+ damping=damping,
132
+ silent=True,
133
+ )
134
+
135
+ current = initial
136
+ rows: list[dict] = []
137
+ initial_error = abs(initial - setpoint)
138
+
139
+ for step in range(1, int(iterations) + 1):
140
+ note = ""
141
+
142
+ # Apply disturbance
143
+ if disturbance_step > 0 and step == int(disturbance_step):
144
+ current += disturbance_magnitude
145
+ note = f"Disturbance: {disturbance_magnitude:+.1f}"
146
+
147
+ error_before = current - setpoint
148
+ correction = loop.apply(current)
149
+ current = correction # apply() returns the corrected value
150
+
151
+ error_after = current - setpoint
152
+
153
+ rows.append({
154
+ "step": step,
155
+ "value": current,
156
+ "error": error_after,
157
+ "correction": current - (error_before + setpoint),
158
+ "note": note,
159
+ })
160
+
161
+ # ── Convergence analysis ──────────────────────────────────────────
162
+ final_error = abs(rows[-1]["error"]) if rows else initial_error
163
+ threshold = max(initial_error * 0.01, 0.01) # 1% of initial distance
164
+ converged = final_error <= threshold
165
+
166
+ converge_step = None
167
+ for r in rows:
168
+ if abs(r["error"]) <= threshold:
169
+ converge_step = r["step"]
170
+ break
171
+
172
+ if converged:
173
+ color, label = "#22c55e", "CONVERGED"
174
+ detail = f"Final error: {final_error:.4f} (within 1% of initial distance {initial_error:.2f})"
175
+ if converge_step:
176
+ detail += f" — converged at step {converge_step}"
177
+ else:
178
+ color, label = "#ef4444", "NOT CONVERGED"
179
+ detail = f"Final error: {final_error:.4f} (threshold: {threshold:.4f})"
180
+
181
+ banner = (
182
+ f'<div style="padding:12px 16px;border-radius:8px;'
183
+ f"background:{color}20;border:2px solid {color};margin-bottom:8px\">"
184
+ f'<span style="font-size:1.3em;font-weight:700;color:{color}">'
185
+ f"{label}</span><br>"
186
+ f'<span style="color:#888;font-size:0.9em">{detail}</span></div>'
187
+ )
188
+
189
+ # ── Timeline table ────────────────────────────────────────────────
190
+ lines = ["| Step | Value | Error | Correction | Notes |", "| ---: | ---: | ---: | ---: | :--- |"]
191
+ for r in rows:
192
+ lines.append(
193
+ f"| {r['step']} | {r['value']:.4f} | {r['error']:.4f} "
194
+ f"| {r['correction']:.4f} | {r['note']} |"
195
+ )
196
+ timeline_md = "\n".join(lines)
197
+
198
+ # ── Analysis ──────────────────────────────────────────────────────
199
+ errors = [abs(r["error"]) for r in rows]
200
+ max_error = max(errors) if errors else 0
201
+ min_error = min(errors) if errors else 0
202
+ overshoots = sum(
203
+ 1
204
+ for i in range(1, len(rows))
205
+ if (rows[i]["error"] > 0) != (rows[i - 1]["error"] > 0)
206
+ )
207
+
208
+ analysis = f"""### Loop Analysis
209
+
210
+ | Metric | Value |
211
+ | :--- | :--- |
212
+ | Setpoint | {setpoint:.2f} |
213
+ | Initial value | {initial:.2f} |
214
+ | Initial distance | {initial_error:.2f} |
215
+ | Final value | {rows[-1]['value']:.4f} if rows else 'N/A' |
216
+ | Final error | {final_error:.4f} |
217
+ | Max |error| | {max_error:.4f} |
218
+ | Min |error| | {min_error:.4f} |
219
+ | Zero-crossings | {overshoots} |
220
+ | Converge step | {converge_step or 'N/A'} |
221
+
222
+ ### Parameter Guide
223
+
224
+ - **Gain** ({gain}): Higher gain → faster correction but more oscillation
225
+ - **Damping** ({damping}): Higher damping → smoother approach but slower convergence
226
+ - **Gain > 0.5 with damping ≈ 0**: Expect oscillation (underdamped)
227
+ - **Gain < 0.3 with damping > 0.2**: Expect slow, monotonic approach (overdamped)
228
+ """
229
+
230
+ return banner, timeline_md, analysis
231
+
232
+
233
+ # ── Gradio UI ──────────────────────────────────────────────────────────────
234
+
235
+
236
+ def build_app() -> gr.Blocks:
237
+ with gr.Blocks(title="Feedback Loop Homeostasis") as app:
238
+ gr.Markdown(
239
+ "# ⚖️ Feedback Loop Homeostasis\n"
240
+ "Simulate a **NegativeFeedbackLoop** controlling a value toward a "
241
+ "setpoint. Adjust gain, damping, and disturbance to explore "
242
+ "convergence dynamics."
243
+ )
244
+
245
+ with gr.Row():
246
+ preset_dd = gr.Dropdown(
247
+ choices=list(PRESETS.keys()),
248
+ value="Temperature control",
249
+ label="Preset",
250
+ scale=2,
251
+ )
252
+ run_btn = gr.Button("Run Loop", variant="primary", scale=1)
253
+
254
+ with gr.Row():
255
+ setpoint_sl = gr.Slider(-100, 200, value=72.0, step=0.5, label="Setpoint")
256
+ initial_sl = gr.Slider(-100, 200, value=85.0, step=0.5, label="Initial value")
257
+
258
+ with gr.Row():
259
+ gain_sl = gr.Slider(0.01, 1.0, value=0.3, step=0.01, label="Gain")
260
+ damping_sl = gr.Slider(0.0, 0.5, value=0.05, step=0.01, label="Damping")
261
+ iter_sl = gr.Slider(10, 50, value=30, step=1, label="Iterations")
262
+
263
+ with gr.Row():
264
+ dist_step = gr.Number(value=0, label="Disturbance step (0=none)", precision=0)
265
+ dist_mag = gr.Number(value=0.0, label="Disturbance magnitude")
266
+
267
+ banner_html = gr.HTML(label="Convergence")
268
+ with gr.Row():
269
+ with gr.Column(scale=2):
270
+ timeline_md = gr.Markdown(label="Timeline")
271
+ with gr.Column(scale=1):
272
+ analysis_md = gr.Markdown(label="Analysis")
273
+
274
+ # ── Event wiring ──────────────────────────────────────────────
275
+ preset_dd.change(
276
+ fn=_load_preset,
277
+ inputs=[preset_dd],
278
+ outputs=[setpoint_sl, initial_sl, gain_sl, damping_sl, iter_sl, dist_step, dist_mag],
279
+ )
280
+
281
+ run_btn.click(
282
+ fn=run_feedback,
283
+ inputs=[preset_dd, setpoint_sl, initial_sl, gain_sl, damping_sl, iter_sl, dist_step, dist_mag],
284
+ outputs=[banner_html, timeline_md, analysis_md],
285
+ )
286
+
287
+ return app
288
+
289
+
290
+ if __name__ == "__main__":
291
+ app = build_app()
292
+ app.launch(theme=gr.themes.Soft())
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio>=4.0
2
+ operon-ai