adityss commited on
Commit
0d301ce
·
2 Parent(s): bc5825df75687e

Merge branch 'main' of https://github.com/LO-Kyu/gridmind

Browse files
Dockerfile CHANGED
@@ -32,9 +32,14 @@ COPY dashboard/ ./dashboard/
32
  COPY data/ ./data/
33
  COPY openenv.yaml ./
34
 
35
- # Configure Supervisor
36
- RUN echo "[supervisord]" > /etc/supervisor/conf.d/supervisord.conf && \
 
 
 
 
37
  echo "nodaemon=true" >> /etc/supervisor/conf.d/supervisord.conf && \
 
38
  echo "[program:go-env]" >> /etc/supervisor/conf.d/supervisord.conf && \
39
  echo "command=/usr/local/bin/gridmind-server" >> /etc/supervisor/conf.d/supervisord.conf && \
40
  echo "environment=PORT=7860" >> /etc/supervisor/conf.d/supervisord.conf && \
@@ -42,6 +47,7 @@ RUN echo "[supervisord]" > /etc/supervisor/conf.d/supervisord.conf && \
42
  echo "stdout_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf && \
43
  echo "stderr_logfile=/dev/stderr" >> /etc/supervisor/conf.d/supervisord.conf && \
44
  echo "stderr_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf && \
 
45
  echo "[program:dashboard]" >> /etc/supervisor/conf.d/supervisord.conf && \
46
  echo "command=python -m uvicorn dashboard.server:app --host 0.0.0.0 --port 7861" >> /etc/supervisor/conf.d/supervisord.conf && \
47
  echo "stdout_logfile=/dev/stdout" >> /etc/supervisor/conf.d/supervisord.conf && \
@@ -49,17 +55,13 @@ RUN echo "[supervisord]" > /etc/supervisor/conf.d/supervisord.conf && \
49
  echo "stderr_logfile=/dev/stderr" >> /etc/supervisor/conf.d/supervisord.conf && \
50
  echo "stderr_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf
51
 
52
- # Create run directory for supervisor
53
- RUN mkdir -p /var/run/supervisor /var/log/supervisor && \
54
- chmod 755 /var/run/supervisor /var/log/supervisor
55
 
56
  # Add a non-root user (good practice and required for some HF Spaces configs)
57
- RUN useradd -m -u 1000 user && \
58
- chown -R user:user /app && \
59
- chown -R user:user /var/run/supervisor /var/log/supervisor
60
 
61
- # 7860 = Env Server (main OpenEnv endpoint), 7861 = Dashboard
62
- EXPOSE 7860 7861
63
-
64
- # Run supervisor as root to manage both services (required for multi-process supervision)
65
- CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf", "-n"]
 
32
  COPY data/ ./data/
33
  COPY openenv.yaml ./
34
 
35
+ # Configure Supervisor to use /tmp for socket and pid files (writable by any user)
36
+ RUN echo "[unix_http_server]" > /etc/supervisor/conf.d/supervisord.conf && \
37
+ echo "file=/tmp/supervisor.sock" >> /etc/supervisor/conf.d/supervisord.conf && \
38
+ echo "" >> /etc/supervisor/conf.d/supervisord.conf && \
39
+ echo "[supervisord]" >> /etc/supervisor/conf.d/supervisord.conf && \
40
+ echo "pidfile=/tmp/supervisord.pid" >> /etc/supervisor/conf.d/supervisord.conf && \
41
  echo "nodaemon=true" >> /etc/supervisor/conf.d/supervisord.conf && \
42
+ echo "" >> /etc/supervisor/conf.d/supervisord.conf && \
43
  echo "[program:go-env]" >> /etc/supervisor/conf.d/supervisord.conf && \
44
  echo "command=/usr/local/bin/gridmind-server" >> /etc/supervisor/conf.d/supervisord.conf && \
45
  echo "environment=PORT=7860" >> /etc/supervisor/conf.d/supervisord.conf && \
 
47
  echo "stdout_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf && \
48
  echo "stderr_logfile=/dev/stderr" >> /etc/supervisor/conf.d/supervisord.conf && \
49
  echo "stderr_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf && \
50
+ echo "" >> /etc/supervisor/conf.d/supervisord.conf && \
51
  echo "[program:dashboard]" >> /etc/supervisor/conf.d/supervisord.conf && \
52
  echo "command=python -m uvicorn dashboard.server:app --host 0.0.0.0 --port 7861" >> /etc/supervisor/conf.d/supervisord.conf && \
53
  echo "stdout_logfile=/dev/stdout" >> /etc/supervisor/conf.d/supervisord.conf && \
 
55
  echo "stderr_logfile=/dev/stderr" >> /etc/supervisor/conf.d/supervisord.conf && \
56
  echo "stderr_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf
57
 
58
+ # EXPOSE 7860 only - this is the main OpenEnv API endpoint (reverse proxy + /dashboard)
59
+ # Port 7861 (dashboard) runs internally only and is accessed via /dashboard proxy
60
+ EXPOSE 7860
61
 
62
  # Add a non-root user (good practice and required for some HF Spaces configs)
63
+ RUN useradd -m -u 1000 user && chown -R user:user /app
 
 
64
 
65
+ # Run supervisord to manage both Go server and Python dashboard
66
+ # Using /tmp for socket and pid files (writable by any user, including uid 1000)
67
+ CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
 
 
README.md CHANGED
@@ -21,6 +21,21 @@ license: mit
21
 
22
  ---
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  ## Overview
25
 
26
  GridMind-RL is a reinforcement learning environment for training and evaluating intelligent control policies in industrial building energy management. The environment simulates realistic HVAC control, thermal storage management, batch job scheduling, and demand response scenarios under stochastic electricity pricing and grid stress events.
 
21
 
22
  ---
23
 
24
+ ---
25
+
26
+ ## 🚀 Live Demo
27
+
28
+ | | URL |
29
+ |--|-----|
30
+ | **Environment API** | https://lo-kyu-gridmind.hf.space |
31
+ | **Live Dashboard** | https://lo-kyu-gridmind.hf.space/dashboard |
32
+
33
+ **Quick test:**
34
+ ```bash
35
+ curl https://lo-kyu-gridmind.hf.space/health
36
+ curl https://lo-kyu-gridmind.hf.space/tasks
37
+ ```
38
+
39
  ## Overview
40
 
41
  GridMind-RL is a reinforcement learning environment for training and evaluating intelligent control policies in industrial building energy management. The environment simulates realistic HVAC control, thermal storage management, batch job scheduling, and demand response scenarios under stochastic electricity pricing and grid stress events.
dashboard/static/dashboard.js CHANGED
@@ -1,6 +1,6 @@
1
  /**
2
  * GridMind-RL Dashboard — Premium Chart.js real-time visualization
3
- * Polls /api/state every 500ms and updates all charts + KPIs.
4
  */
5
 
6
  'use strict';
@@ -10,7 +10,7 @@ const POLL_MS = 500;
10
  const EPISODE_STEPS = 96; // 24h × 4 steps/h (15-min)
11
  const HISTORY_LEN = EPISODE_STEPS;
12
  const CURVE_POINTS = 24; // hourly downsample (EpisodeSteps/4)
13
- const API_BASE = '/api';
14
  const TASK_NAMES = {
15
  1: 'Task 1 — Cost Minimization (Easy)',
16
  2: 'Task 2 — Temperature Management (Medium)',
 
1
  /**
2
  * GridMind-RL Dashboard — Premium Chart.js real-time visualization
3
+ * Polls /dashboard/api/state every 500ms and updates all charts + KPIs.
4
  */
5
 
6
  'use strict';
 
10
  const EPISODE_STEPS = 96; // 24h × 4 steps/h (15-min)
11
  const HISTORY_LEN = EPISODE_STEPS;
12
  const CURVE_POINTS = 24; // hourly downsample (EpisodeSteps/4)
13
+ const API_BASE = '/dashboard/api';
14
  const TASK_NAMES = {
15
  1: 'Task 1 — Cost Minimization (Easy)',
16
  2: 'Task 2 — Temperature Management (Medium)',
dashboard/static/index.html CHANGED
@@ -1035,7 +1035,7 @@
1035
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
1036
  Grade Episode
1037
  </button>
1038
- <button class="btn" onclick="window.open('/api/replay')">
1039
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
1040
  Export Replay
1041
  </button>
@@ -1047,10 +1047,10 @@
1047
 
1048
  <footer>
1049
  GridMind-RL &nbsp;·&nbsp; OpenEnv-compliant RL environment for industrial demand response &nbsp;·&nbsp;
1050
- <a href="/api/health" target="_blank">API Health</a> &nbsp;·&nbsp;
1051
- <a href="/api/metrics" target="_blank">Metrics</a>
1052
  </footer>
1053
 
1054
- <script src="/static/dashboard.js"></script>
1055
  </body>
1056
  </html>
 
1035
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
1036
  Grade Episode
1037
  </button>
1038
+ <button class="btn" onclick="window.open('/dashboard/api/replay')">
1039
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
1040
  Export Replay
1041
  </button>
 
1047
 
1048
  <footer>
1049
  GridMind-RL &nbsp;·&nbsp; OpenEnv-compliant RL environment for industrial demand response &nbsp;·&nbsp;
1050
+ <a href="/dashboard/api/health" target="_blank">API Health</a> &nbsp;·&nbsp;
1051
+ <a href="/dashboard/api/metrics" target="_blank">Metrics</a>
1052
  </footer>
1053
 
1054
+ <script src="/dashboard/static/dashboard.js"></script>
1055
  </body>
1056
  </html>
main.go CHANGED
@@ -8,9 +8,13 @@ import (
8
  "fmt"
9
  "log"
10
  "math"
 
11
  "net/http"
 
 
12
  "os"
13
  "strconv"
 
14
  "sync"
15
  "sync/atomic"
16
  "time"
@@ -32,8 +36,8 @@ type Metrics struct {
32
  rewardMin float64
33
  rewardMax float64
34
  // Histograms
35
- actionBuckets map[string]int64 // hvac bucket counts
36
- errorCount int64
37
  }
38
 
39
  var metrics = &Metrics{
@@ -134,6 +138,7 @@ func newServer() *Server {
134
 
135
  func (s *Server) routes() *http.ServeMux {
136
  mux := http.NewServeMux()
 
137
  mux.HandleFunc("/health", s.handleHealth)
138
  mux.HandleFunc("/ping", s.handlePing)
139
  mux.HandleFunc("/reset", s.handleReset)
@@ -143,9 +148,63 @@ func (s *Server) routes() *http.ServeMux {
143
  mux.HandleFunc("/grade", s.handleGrade)
144
  mux.HandleFunc("/tasks", s.handleTasks)
145
  mux.HandleFunc("/metrics", s.handleMetrics)
 
 
 
146
  return mux
147
  }
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  // ── /health ──────────────────────────────────────────────────────────────────
150
 
151
  func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
@@ -333,6 +392,58 @@ func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) {
333
  fmt.Fprint(w, metrics.prometheus())
334
  }
335
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  // ──────────────────────────────────────────────
337
  // Entry point
338
  // ──────────────────────────────────────────────
@@ -354,7 +465,7 @@ func main() {
354
  srv.envMgr.Reset(env.ResetRequest{Seed: &seed, TaskID: 1, NumBuildings: 1})
355
 
356
  log.Printf("GridMind-RL environment server starting on :%s", port)
357
- log.Printf("Endpoints: GET /health /ping /state /replay /grade /tasks /metrics | POST /reset /step")
358
 
359
  mux := withCORS(withLogging(srv.routes()))
360
  if err := http.ListenAndServe(":"+port, mux); err != nil {
 
8
  "fmt"
9
  "log"
10
  "math"
11
+ "net"
12
  "net/http"
13
+ "net/http/httputil"
14
+ "net/url"
15
  "os"
16
  "strconv"
17
+ "strings"
18
  "sync"
19
  "sync/atomic"
20
  "time"
 
36
  rewardMin float64
37
  rewardMax float64
38
  // Histograms
39
+ actionBuckets map[string]int64 // hvac bucket counts
40
+ errorCount int64
41
  }
42
 
43
  var metrics = &Metrics{
 
138
 
139
  func (s *Server) routes() *http.ServeMux {
140
  mux := http.NewServeMux()
141
+ mux.HandleFunc("/", s.handleRoot)
142
  mux.HandleFunc("/health", s.handleHealth)
143
  mux.HandleFunc("/ping", s.handlePing)
144
  mux.HandleFunc("/reset", s.handleReset)
 
148
  mux.HandleFunc("/grade", s.handleGrade)
149
  mux.HandleFunc("/tasks", s.handleTasks)
150
  mux.HandleFunc("/metrics", s.handleMetrics)
151
+ // Reverse proxy for dashboard (runs on port 7861 internally)
152
+ mux.HandleFunc("/dashboard", s.handleDashboardProxy)
153
+ mux.HandleFunc("/dashboard/", s.handleDashboardProxy)
154
  return mux
155
  }
156
 
157
+ // ── / (Root) ────────────────────────────────────────────────────────────────
158
+
159
+ func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
160
+ if r.URL.Path != "/" {
161
+ http.NotFound(w, r)
162
+ return
163
+ }
164
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
165
+ html := `<!DOCTYPE html>
166
+ <html>
167
+ <head><title>GridMind-RL</title>
168
+ <style>
169
+ body { font-family: monospace; background: #0d1117; color: #e6edf3; padding: 40px; }
170
+ a { color: #58a6ff; text-decoration: none; }
171
+ a:hover { text-decoration: underline; }
172
+ .badge { background: #238636; padding: 4px 10px; border-radius: 4px; color: white; display: inline-block; }
173
+ ul { line-height: 1.8; }
174
+ pre { background: #0d1117; border: 1px solid #30363d; padding: 16px; border-radius: 6px; overflow-x: auto; }
175
+ </style>
176
+ </head>
177
+ <body>
178
+ <h1>⚡ GridMind-RL</h1>
179
+ <p><span class="badge">● RUNNING</span></p>
180
+ <p>Industrial building energy management RL environment — OpenEnv compliant.</p>
181
+ <h3>🔗 Quick Links</h3>
182
+ <ul>
183
+ <li><a href="/dashboard">→ Live Dashboard</a></li>
184
+ <li><a href="/health">→ API Health Check</a></li>
185
+ <li><a href="/tasks">→ Available Tasks</a></li>
186
+ <li><a href="/metrics">→ Prometheus Metrics</a></li>
187
+ </ul>
188
+ <h3>📡 API Endpoints</h3>
189
+ <pre>GET /health → health check
190
+ GET /ping → ping pong
191
+ GET /state → current environment state
192
+ GET /replay → episode replay data
193
+ GET /grade → episode grade score
194
+ GET /tasks → list of tasks
195
+ GET /metrics → prometheus metrics
196
+ POST /reset {task_id} → start new episode
197
+ POST /step {action} → take action</pre>
198
+ <h3>📚 Links</h3>
199
+ <ul>
200
+ <li><a href="https://github.com/LO-Kyu/gridmind">GitHub Repository</a></li>
201
+ <li><a href="https://openenv.org">OpenEnv Specification</a></li>
202
+ </ul>
203
+ </body>
204
+ </html>`
205
+ w.Write([]byte(html))
206
+ }
207
+
208
  // ── /health ──────────────────────────────────────────────────────────────────
209
 
210
  func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
 
392
  fmt.Fprint(w, metrics.prometheus())
393
  }
394
 
395
+ // ── /dashboard (Reverse Proxy) ────────────────────────────────────────────
396
+
397
+ func (s *Server) handleDashboardProxy(w http.ResponseWriter, r *http.Request) {
398
+ // Target URL for the dashboard service (running on localhost:7861)
399
+ target, err := url.Parse("http://localhost:7861")
400
+ if err != nil {
401
+ http.Error(w, "proxy configuration error", http.StatusInternalServerError)
402
+ return
403
+ }
404
+
405
+ // Create a custom director to modify the request
406
+ director := func(req *http.Request) {
407
+ // Strip /dashboard prefix
408
+ path := req.URL.Path
409
+ if strings.HasPrefix(path, "/dashboard") {
410
+ path = strings.TrimPrefix(path, "/dashboard")
411
+ if path == "" {
412
+ path = "/"
413
+ }
414
+ }
415
+
416
+ // Set up the new request
417
+ req.URL.Scheme = target.Scheme
418
+ req.URL.Host = target.Host
419
+ if target.Path != "" {
420
+ req.URL.Path = target.Path + path
421
+ } else {
422
+ req.URL.Path = path
423
+ }
424
+ req.RequestURI = ""
425
+
426
+ // Preserve original host header for dashboard API calls
427
+ if req.Header.Get("X-Forwarded-Host") == "" {
428
+ req.Header.Set("X-Forwarded-For", getClientIP(r))
429
+ req.Header.Set("X-Forwarded-Proto", "https")
430
+ }
431
+ }
432
+
433
+ // Use ReverseProxy with custom director
434
+ proxy := &httputil.ReverseProxy{Director: director}
435
+ proxy.ServeHTTP(w, r)
436
+ }
437
+
438
+ // Helper: extract client IP from request
439
+ func getClientIP(r *http.Request) string {
440
+ ip, _, err := net.SplitHostPort(r.RemoteAddr)
441
+ if err != nil {
442
+ return r.RemoteAddr
443
+ }
444
+ return ip
445
+ }
446
+
447
  // ──────────────────────────────────────────────
448
  // Entry point
449
  // ──────────────────────────────────────────────
 
465
  srv.envMgr.Reset(env.ResetRequest{Seed: &seed, TaskID: 1, NumBuildings: 1})
466
 
467
  log.Printf("GridMind-RL environment server starting on :%s", port)
468
+ log.Printf("Endpoints: GET / (landing) | GET /health /ping /state /replay /grade /tasks /metrics /dashboard | POST /reset /step")
469
 
470
  mux := withCORS(withLogging(srv.routes()))
471
  if err := http.ListenAndServe(":"+port, mux); err != nil {
python/inference.py CHANGED
@@ -376,6 +376,8 @@ def run_episode(
376
  grade = env_client.grade()
377
 
378
  # Emit [END] with required fields
 
 
379
  print(
380
  f"[END] success={'true' if success else 'false'} steps={total_steps} rewards={rewards_str}",
381
  flush=True
 
376
  grade = env_client.grade()
377
 
378
  # Emit [END] with required fields
379
+ success = last_error is None and step_resp.get("done", False)
380
+ rewards_str = ",".join(f"{r:.2f}" for r in step_rewards)
381
  print(
382
  f"[END] success={'true' if success else 'false'} steps={total_steps} rewards={rewards_str}",
383
  flush=True