Sheldon123z commited on
Commit
03a8ca6
·
verified ·
1 Parent(s): 9413360

Deploy PowerZoo-VVC HuggingFace Space

Browse files
Files changed (4) hide show
  1. README.md +21 -5
  2. __pycache__/app.cpython-310.pyc +0 -0
  3. app.py +857 -0
  4. requirements.txt +4 -0
README.md CHANGED
@@ -1,12 +1,28 @@
1
  ---
2
- title: PowerZoo VVC
3
- emoji: 👀
4
- colorFrom: gray
5
  colorTo: blue
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: PowerZoo VVC - Volt-VAR Control
3
+ emoji:
4
+ colorFrom: indigo
5
  colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 4.44.1
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
+ tags:
12
+ - reinforcement-learning
13
+ - multi-agent
14
+ - power-systems
15
+ - volt-var-control
16
+ - OpenDSS
17
  ---
18
 
19
+ # PowerZoo VVC: Volt-VAR Control Environment
20
+
21
+ Interactive demo for the VVC (Volt-VAR Control) environment in PowerZoo.
22
+
23
+ Features 5-20 homogeneous agents controlling capacitors, regulators, batteries, and PV systems
24
+ on IEEE 13/34/123 Bus test systems with OpenDSS backend.
25
+
26
+ **Paper**: IEEE Transactions on Smart Grid, 2025
27
+
28
+ **GitHub**: [PowerZoo Repository](https://github.com/XJTU-RL/PowerZoo)
__pycache__/app.cpython-310.pyc ADDED
Binary file (21.4 kB). View file
 
app.py ADDED
@@ -0,0 +1,857 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PowerZoo VVC (Volt-VAR Control) Environment Demo
3
+ HuggingFace Space - Self-contained Gradio + Plotly application.
4
+
5
+ 5 Tabs: Overview | Voltage Profile | Device Schedule | Reward Analysis | Training Dashboard
6
+ """
7
+ import gradio as gr
8
+ import numpy as np
9
+ import pandas as pd
10
+ import plotly.graph_objects as go
11
+ from plotly.subplots import make_subplots
12
+
13
+ # === Monkey-patch: fix Gradio 6.x + Plotly additionalProperties schema error ===
14
+ _original_plot_init = gr.Plot.__init__
15
+
16
+
17
+ def _patched_plot_init(self, *args, **kwargs):
18
+ _original_plot_init(self, *args, **kwargs)
19
+ if hasattr(self, "schema") and isinstance(self.schema, dict):
20
+ self.schema.pop("additionalProperties", None)
21
+
22
+
23
+ gr.Plot.__init__ = _patched_plot_init
24
+
25
+
26
+ # ============================================================
27
+ # Color Palette & Theme
28
+ # ============================================================
29
+ COLORS = {
30
+ "primary": "#6366F1",
31
+ "secondary": "#8B5CF6",
32
+ "accent": "#22D3EE",
33
+ "warning": "#F59E0B",
34
+ "danger": "#EF4444",
35
+ "success": "#10B981",
36
+ "bg": "#0F172A",
37
+ "surface": "#1E293B",
38
+ "text": "#E2E8F0",
39
+ "muted": "#94A3B8",
40
+ "agents": ["#6366F1", "#8B5CF6", "#22D3EE", "#F59E0B", "#EF4444", "#10B981"],
41
+ }
42
+
43
+ PLOTLY_LAYOUT = dict(
44
+ template="plotly_dark",
45
+ paper_bgcolor=COLORS["bg"],
46
+ plot_bgcolor=COLORS["surface"],
47
+ font=dict(color=COLORS["text"], family="Inter, sans-serif"),
48
+ margin=dict(l=50, r=30, t=50, b=50),
49
+ hoverlabel=dict(bgcolor=COLORS["surface"], font_color=COLORS["text"]),
50
+ )
51
+
52
+
53
+ # ============================================================
54
+ # Demo Data Generators
55
+ # ============================================================
56
+ # All data is deterministic (seeded) so the demo is reproducible.
57
+
58
+
59
+ def _seed() -> np.random.Generator:
60
+ """Return a seeded random generator for reproducible demo data."""
61
+ return np.random.default_rng(42)
62
+
63
+
64
+ # --- IEEE 13-Bus names ---
65
+ BUS_NAMES: list[str] = [
66
+ "650", "632", "633", "634", "645", "646", "671",
67
+ "680", "684", "611", "652", "692", "675",
68
+ ]
69
+
70
+ # --- Base voltage profile (pu) for 13 buses at noon ---
71
+ _BASE_VOLTAGES = np.array([
72
+ 1.040, 1.025, 1.018, 1.012, 1.008, 1.005, 0.990,
73
+ 0.985, 0.978, 0.965, 0.958, 0.992, 0.988,
74
+ ])
75
+
76
+
77
+ def generate_voltage_profile(step: int) -> np.ndarray:
78
+ """Generate realistic 13-bus voltage magnitudes for a given hour (0-23).
79
+
80
+ Night hours (0-6, 20-23): slightly lower voltages due to light load.
81
+ Midday (10-14): PV injection pushes upstream buses high, downstream stays moderate.
82
+ Evening peak (17-19): heavy load sags voltage.
83
+ """
84
+ rng = np.random.default_rng(step * 137 + 7)
85
+ hour_offset = np.zeros(13)
86
+
87
+ if 0 <= step <= 5:
88
+ # Night: low load, voltages drift slightly below nominal
89
+ hour_offset = np.array([
90
+ -0.005, -0.008, -0.010, -0.012, -0.015, -0.016, -0.020,
91
+ -0.022, -0.025, -0.030, -0.032, -0.018, -0.020,
92
+ ])
93
+ elif 6 <= step <= 9:
94
+ # Morning ramp: load increases, PV starts
95
+ t = (step - 6) / 3.0
96
+ hour_offset = np.array([
97
+ 0.002, 0.000, -0.002, -0.005, -0.008, -0.010, -0.015,
98
+ -0.018, -0.020, -0.025, -0.028, -0.012, -0.014,
99
+ ]) * (1.0 - 0.5 * t)
100
+ elif 10 <= step <= 14:
101
+ # Midday peak PV: upstream voltages rise, downstream moderate
102
+ hour_offset = np.array([
103
+ 0.010, 0.008, 0.005, 0.003, 0.000, -0.002, -0.008,
104
+ -0.010, -0.015, -0.020, -0.022, -0.005, -0.008,
105
+ ])
106
+ elif 15 <= step <= 16:
107
+ # Afternoon transition
108
+ hour_offset = np.array([
109
+ 0.005, 0.002, -0.002, -0.006, -0.010, -0.012, -0.018,
110
+ -0.020, -0.024, -0.028, -0.030, -0.015, -0.018,
111
+ ])
112
+ elif 17 <= step <= 19:
113
+ # Evening peak: heavy load, voltage sags
114
+ hour_offset = np.array([
115
+ -0.008, -0.012, -0.018, -0.022, -0.028, -0.030, -0.038,
116
+ -0.042, -0.048, -0.055, -0.058, -0.035, -0.040,
117
+ ])
118
+ else:
119
+ # Late evening (20-23): load decreasing
120
+ hour_offset = np.array([
121
+ -0.003, -0.006, -0.009, -0.012, -0.016, -0.018, -0.024,
122
+ -0.028, -0.032, -0.038, -0.040, -0.022, -0.025,
123
+ ])
124
+
125
+ noise = rng.normal(0, 0.003, size=13)
126
+ return _BASE_VOLTAGES + hour_offset + noise
127
+
128
+
129
+ def generate_device_schedules() -> dict[str, np.ndarray]:
130
+ """Generate 24-step device operation profiles.
131
+
132
+ Returns dict with keys:
133
+ cap1, cap2: (24,) int {0, 1}
134
+ reg1, reg2: (24,) int [0, 16]
135
+ battery_kw: (24,) float (negative=charge, positive=discharge)
136
+ pv_output_kw: (24,) float
137
+ pv_curtail_kw: (24,) float
138
+ """
139
+ rng = _seed()
140
+ hours = np.arange(24)
141
+
142
+ # Capacitors: on during high-load periods
143
+ cap1 = np.zeros(24, dtype=int)
144
+ cap1[7:21] = 1
145
+ cap1[12:14] = 0 # Brief switch during midday PV peak
146
+ cap2 = np.zeros(24, dtype=int)
147
+ cap2[9:20] = 1
148
+
149
+ # Regulators: tap varies with voltage needs
150
+ reg1_base = 8 * np.ones(24, dtype=int)
151
+ reg1_base[0:6] = 10
152
+ reg1_base[6:10] = 9
153
+ reg1_base[10:15] = 6
154
+ reg1_base[15:17] = 8
155
+ reg1_base[17:20] = 12
156
+ reg1_base[20:24] = 10
157
+ reg1 = np.clip(reg1_base + rng.integers(-1, 2, size=24), 0, 16)
158
+
159
+ reg2_base = 7 * np.ones(24, dtype=int)
160
+ reg2_base[0:6] = 9
161
+ reg2_base[10:15] = 5
162
+ reg2_base[17:20] = 11
163
+ reg2 = np.clip(reg2_base + rng.integers(-1, 2, size=24), 0, 16)
164
+
165
+ # Battery: charge from PV midday, discharge evening peak
166
+ battery_kw = np.zeros(24)
167
+ battery_kw[10:14] = -np.array([80, 120, 130, 100]) # Charge
168
+ battery_kw[17:21] = np.array([100, 140, 120, 60]) # Discharge
169
+ battery_kw += rng.normal(0, 5, size=24)
170
+ battery_kw[:6] = rng.normal(0, 3, size=6)
171
+
172
+ # PV output: bell curve peaking at noon
173
+ pv_max = 350.0
174
+ solar_envelope = pv_max * np.exp(-0.5 * ((hours - 12.5) / 3.0) ** 2)
175
+ solar_envelope[:6] = 0
176
+ solar_envelope[20:] = 0
177
+ cloud_factor = np.ones(24)
178
+ cloud_factor[9] = 0.6
179
+ cloud_factor[13] = 0.75
180
+ pv_output_kw = solar_envelope * cloud_factor + rng.normal(0, 5, size=24)
181
+ pv_output_kw = np.clip(pv_output_kw, 0, pv_max)
182
+
183
+ # PV curtailment: agent reduces output during overvoltage
184
+ pv_curtail_kw = np.zeros(24)
185
+ pv_curtail_kw[11:14] = np.array([20, 45, 30])
186
+ pv_curtail_kw += rng.uniform(0, 5, size=24)
187
+ pv_curtail_kw = np.clip(pv_curtail_kw, 0, pv_output_kw * 0.3)
188
+
189
+ return {
190
+ "cap1": cap1,
191
+ "cap2": cap2,
192
+ "reg1": reg1,
193
+ "reg2": reg2,
194
+ "battery_kw": battery_kw,
195
+ "pv_output_kw": pv_output_kw,
196
+ "pv_curtail_kw": pv_curtail_kw,
197
+ }
198
+
199
+
200
+ def generate_reward_data() -> dict[str, np.ndarray]:
201
+ """Generate 24-step reward component data.
202
+
203
+ Reward components (all negative, closer to 0 is better):
204
+ power_loss: proportional to line losses
205
+ voltage_violation: penalty for out-of-band voltages
206
+ control_penalty: penalty for device switching
207
+ """
208
+ rng = _seed()
209
+ hours = np.arange(24)
210
+
211
+ # Power loss: moderate baseline, higher during peak
212
+ loss_base = -0.3 * np.ones(24)
213
+ loss_base[17:20] = -0.6 # Evening peak
214
+ loss_base[10:14] = -0.2 # PV reduces loss
215
+ power_loss = loss_base + rng.normal(0, 0.03, size=24)
216
+
217
+ # Voltage violation: high early morning & evening, low midday
218
+ vv_base = np.zeros(24)
219
+ vv_base[0:6] = -0.15
220
+ vv_base[17:20] = -0.35
221
+ vv_base[20:24] = -0.12
222
+ vv_base[10:14] = -0.05
223
+ voltage_violation = vv_base + rng.normal(0, 0.02, size=24)
224
+ voltage_violation = np.clip(voltage_violation, -1.0, 0.0)
225
+
226
+ # Control penalty: spike when devices switch
227
+ control_penalty = rng.uniform(-0.05, 0.0, size=24)
228
+ control_penalty[7] = -0.20 # Cap switch-on
229
+ control_penalty[12] = -0.15 # Cap toggle
230
+ control_penalty[17] = -0.18 # Reg big tap change
231
+ control_penalty[21] = -0.12 # Cap switch-off
232
+
233
+ return {
234
+ "power_loss": power_loss,
235
+ "voltage_violation": voltage_violation,
236
+ "control_penalty": control_penalty,
237
+ }
238
+
239
+
240
+ def generate_training_data() -> dict[str, np.ndarray]:
241
+ """Generate synthetic HAPPO training curves (2000 episodes).
242
+
243
+ Returns dict with:
244
+ episodes: (2000,) int
245
+ episode_rewards: (2000,) float - total reward per episode
246
+ agent_policy_loss: (2000, 6) float - per-agent policy loss
247
+ power_loss_kw: (2000,) float - episode-mean power loss
248
+ """
249
+ rng = _seed()
250
+ n_ep = 2000
251
+ episodes = np.arange(n_ep)
252
+
253
+ # Episode reward: starts around -15, converges to ~ -4
254
+ # Exponential decay + noise
255
+ converged = -4.0
256
+ initial = -15.0
257
+ tau = 400.0 # Decay constant
258
+ base_curve = converged + (initial - converged) * np.exp(-episodes / tau)
259
+ noise = rng.normal(0, 0.8, size=n_ep)
260
+ # Smoothed noise for realistic jitter
261
+ kernel = np.ones(20) / 20.0
262
+ smooth_noise = np.convolve(noise, kernel, mode="same")
263
+ episode_rewards = base_curve + smooth_noise
264
+
265
+ # Per-agent policy loss: 6 agents, each converges differently
266
+ agent_policy_loss = np.zeros((n_ep, 6))
267
+ for i in range(6):
268
+ agent_tau = 300 + i * 60
269
+ agent_init = 2.5 + rng.uniform(-0.3, 0.3)
270
+ agent_final = 0.3 + rng.uniform(-0.05, 0.05)
271
+ agent_curve = agent_final + (agent_init - agent_final) * np.exp(-episodes / agent_tau)
272
+ agent_noise = rng.normal(0, 0.15, size=n_ep)
273
+ agent_smooth = np.convolve(agent_noise, kernel, mode="same")
274
+ agent_policy_loss[:, i] = agent_curve + agent_smooth
275
+
276
+ # Power loss reduction: starts ~180 kW, drops to ~90 kW
277
+ pl_init = 180.0
278
+ pl_final = 90.0
279
+ pl_tau = 500.0
280
+ power_loss_kw = pl_final + (pl_init - pl_final) * np.exp(-episodes / pl_tau)
281
+ power_loss_kw += rng.normal(0, 5.0, size=n_ep)
282
+
283
+ return {
284
+ "episodes": episodes,
285
+ "episode_rewards": episode_rewards,
286
+ "agent_policy_loss": agent_policy_loss,
287
+ "power_loss_kw": power_loss_kw,
288
+ }
289
+
290
+
291
+ # Pre-generate all demo data
292
+ DEVICE_DATA = generate_device_schedules()
293
+ REWARD_DATA = generate_reward_data()
294
+ TRAINING_DATA = generate_training_data()
295
+
296
+
297
+ # ============================================================
298
+ # Plot Factory Functions
299
+ # ============================================================
300
+
301
+
302
+ def plot_voltage_profile(step: int = 12) -> go.Figure:
303
+ """Create interactive bar chart of 13-bus voltage magnitudes.
304
+
305
+ Args:
306
+ step: Hour of day (0-23).
307
+
308
+ Returns:
309
+ Plotly Figure with colored bars and reference lines.
310
+ """
311
+ voltages = generate_voltage_profile(step)
312
+
313
+ # Color coding by voltage status
314
+ bar_colors = []
315
+ for v in voltages:
316
+ if 0.95 <= v <= 1.05:
317
+ bar_colors.append(COLORS["success"])
318
+ elif (0.93 <= v < 0.95) or (1.05 < v <= 1.07):
319
+ bar_colors.append(COLORS["warning"])
320
+ else:
321
+ bar_colors.append(COLORS["danger"])
322
+
323
+ fig = go.Figure()
324
+
325
+ fig.add_trace(go.Bar(
326
+ x=BUS_NAMES,
327
+ y=voltages,
328
+ marker=dict(color=bar_colors, line=dict(width=1, color=COLORS["muted"])),
329
+ text=[f"{v:.4f}" for v in voltages],
330
+ textposition="outside",
331
+ textfont=dict(size=10, color=COLORS["text"]),
332
+ hovertemplate="Bus %{x}<br>Voltage: %{y:.4f} pu<extra></extra>",
333
+ ))
334
+
335
+ # Reference lines
336
+ fig.add_hline(y=1.05, line_dash="dash", line_color=COLORS["warning"],
337
+ annotation_text="Upper limit (1.05)", annotation_position="top right",
338
+ annotation_font_color=COLORS["warning"])
339
+ fig.add_hline(y=0.95, line_dash="dash", line_color=COLORS["warning"],
340
+ annotation_text="Lower limit (0.95)", annotation_position="bottom right",
341
+ annotation_font_color=COLORS["warning"])
342
+
343
+ # Color legend via invisible traces
344
+ for label, color in [("Normal (0.95-1.05)", COLORS["success"]),
345
+ ("Warning", COLORS["warning"]),
346
+ ("Violation", COLORS["danger"])]:
347
+ fig.add_trace(go.Bar(
348
+ x=[None], y=[None],
349
+ marker=dict(color=color),
350
+ name=label,
351
+ showlegend=True,
352
+ ))
353
+
354
+ fig.update_layout(
355
+ **PLOTLY_LAYOUT,
356
+ title=f"IEEE 13-Bus Voltage Profile - Hour {step}:00",
357
+ xaxis_title="Bus ID",
358
+ yaxis_title="Voltage Magnitude (pu)",
359
+ yaxis=dict(range=[0.92, 1.08]),
360
+ height=520,
361
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
362
+ bargap=0.15,
363
+ )
364
+ return fig
365
+
366
+
367
+ def plot_device_schedule() -> go.Figure:
368
+ """Create 2x2 subplot showing 24-step device operations.
369
+
370
+ Subplots:
371
+ 1. Capacitor Status (step plot, on/off)
372
+ 2. Regulator Tap Position (line plot, 0-16)
373
+ 3. Battery Power (bar chart, charge/discharge)
374
+ 4. PV Output with curtailment shading (area chart)
375
+ """
376
+ hours = list(range(24))
377
+ d = DEVICE_DATA
378
+
379
+ fig = make_subplots(
380
+ rows=2, cols=2,
381
+ subplot_titles=(
382
+ "Capacitor Status", "Regulator Tap Position",
383
+ "Battery Power (kW)", "PV Output & Curtailment (kW)",
384
+ ),
385
+ vertical_spacing=0.14,
386
+ horizontal_spacing=0.10,
387
+ )
388
+
389
+ # --- Subplot 1: Capacitors (step plot) ---
390
+ for name, data, color, offset in [
391
+ ("Cap 1", d["cap1"], COLORS["primary"], 0),
392
+ ("Cap 2", d["cap2"], COLORS["accent"], 0),
393
+ ]:
394
+ fig.add_trace(go.Scatter(
395
+ x=hours, y=data,
396
+ mode="lines",
397
+ name=name,
398
+ line=dict(shape="hv", color=color, width=2.5),
399
+ legendgroup="cap",
400
+ ), row=1, col=1)
401
+
402
+ fig.update_yaxes(tickvals=[0, 1], ticktext=["OFF", "ON"], range=[-0.1, 1.3], row=1, col=1)
403
+
404
+ # --- Subplot 2: Regulators (line plot) ---
405
+ for name, data, color in [
406
+ ("Reg 1", d["reg1"], COLORS["secondary"]),
407
+ ("Reg 2", d["reg2"], COLORS["warning"]),
408
+ ]:
409
+ fig.add_trace(go.Scatter(
410
+ x=hours, y=data,
411
+ mode="lines+markers",
412
+ name=name,
413
+ line=dict(color=color, width=2),
414
+ marker=dict(size=5),
415
+ legendgroup="reg",
416
+ ), row=1, col=2)
417
+
418
+ fig.update_yaxes(range=[-0.5, 16.5], dtick=4, row=1, col=2)
419
+
420
+ # --- Subplot 3: Battery (bar chart) ---
421
+ bat_colors = [COLORS["accent"] if v >= 0 else COLORS["secondary"] for v in d["battery_kw"]]
422
+ fig.add_trace(go.Bar(
423
+ x=hours, y=d["battery_kw"],
424
+ name="Battery",
425
+ marker=dict(color=bat_colors, line=dict(width=0.5, color=COLORS["muted"])),
426
+ showlegend=True,
427
+ legendgroup="bat",
428
+ hovertemplate="Hour %{x}<br>Power: %{y:.1f} kW<extra></extra>",
429
+ ), row=2, col=1)
430
+
431
+ fig.add_hline(y=0, line_dash="dot", line_color=COLORS["muted"], row=2, col=1)
432
+
433
+ # --- Subplot 4: PV Output (area) + Curtailment shading ---
434
+ net_pv = d["pv_output_kw"] - d["pv_curtail_kw"]
435
+
436
+ # Available (total) as upper envelope
437
+ fig.add_trace(go.Scatter(
438
+ x=hours, y=d["pv_output_kw"],
439
+ mode="lines",
440
+ name="PV Available",
441
+ line=dict(color=COLORS["warning"], width=1, dash="dot"),
442
+ fill="tozeroy",
443
+ fillcolor="rgba(245, 158, 11, 0.15)",
444
+ legendgroup="pv",
445
+ ), row=2, col=2)
446
+
447
+ # Actual output (after curtailment) as solid area
448
+ fig.add_trace(go.Scatter(
449
+ x=hours, y=net_pv,
450
+ mode="lines",
451
+ name="PV Delivered",
452
+ line=dict(color=COLORS["warning"], width=2.5),
453
+ fill="tozeroy",
454
+ fillcolor="rgba(245, 158, 11, 0.35)",
455
+ legendgroup="pv",
456
+ ), row=2, col=2)
457
+
458
+ # Global layout
459
+ fig.update_layout(
460
+ **PLOTLY_LAYOUT,
461
+ height=680,
462
+ title_text="24-Hour Device Operation Schedule",
463
+ legend=dict(
464
+ orientation="h", yanchor="bottom", y=1.02, xanchor="center", x=0.5,
465
+ font=dict(size=11),
466
+ ),
467
+ )
468
+
469
+ # Common x-axis styling
470
+ for row in [1, 2]:
471
+ for col in [1, 2]:
472
+ fig.update_xaxes(title_text="Hour", dtick=4, row=row, col=col)
473
+
474
+ return fig
475
+
476
+
477
+ def plot_reward_analysis() -> go.Figure:
478
+ """Create stacked bar chart of reward components with cumulative line.
479
+
480
+ Left y-axis: stacked bars (power_loss + voltage_violation + control_penalty).
481
+ Right y-axis: cumulative total reward line.
482
+ """
483
+ hours = list(range(24))
484
+ r = REWARD_DATA
485
+
486
+ fig = make_subplots(specs=[[{"secondary_y": True}]])
487
+
488
+ # Stacked bars (all negative values)
489
+ for name, data, color in [
490
+ ("Power Loss", r["power_loss"], COLORS["primary"]),
491
+ ("Voltage Violation", r["voltage_violation"], COLORS["danger"]),
492
+ ("Control Penalty", r["control_penalty"], COLORS["warning"]),
493
+ ]:
494
+ fig.add_trace(go.Bar(
495
+ x=hours, y=data,
496
+ name=name,
497
+ marker=dict(color=color, opacity=0.85),
498
+ hovertemplate=f"{name}<br>Hour %{{x}}: %{{y:.3f}}<extra></extra>",
499
+ ), secondary_y=False)
500
+
501
+ # Cumulative total reward line
502
+ total_per_step = r["power_loss"] + r["voltage_violation"] + r["control_penalty"]
503
+ cumulative = np.cumsum(total_per_step)
504
+
505
+ fig.add_trace(go.Scatter(
506
+ x=hours, y=cumulative,
507
+ mode="lines+markers",
508
+ name="Cumulative Reward",
509
+ line=dict(color=COLORS["accent"], width=3),
510
+ marker=dict(size=6, symbol="diamond"),
511
+ hovertemplate="Hour %{x}<br>Cumulative: %{y:.2f}<extra></extra>",
512
+ ), secondary_y=True)
513
+
514
+ fig.update_layout(
515
+ **PLOTLY_LAYOUT,
516
+ barmode="relative",
517
+ height=520,
518
+ title="Episode Reward Decomposition (24 Steps)",
519
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="center", x=0.5),
520
+ bargap=0.12,
521
+ )
522
+
523
+ fig.update_xaxes(title_text="Hour", dtick=2)
524
+ fig.update_yaxes(title_text="Step Reward", secondary_y=False)
525
+ fig.update_yaxes(title_text="Cumulative Reward", secondary_y=True,
526
+ gridcolor="rgba(148, 163, 184, 0.1)")
527
+
528
+ return fig
529
+
530
+
531
+ def plot_training_rewards() -> go.Figure:
532
+ """Plot episode reward curve over 2000 episodes with rolling mean."""
533
+ t = TRAINING_DATA
534
+ ep = t["episodes"]
535
+ rw = t["episode_rewards"]
536
+
537
+ # Rolling mean (window=50)
538
+ window = 50
539
+ rolling = np.convolve(rw, np.ones(window) / window, mode="valid")
540
+ rolling_x = ep[window - 1:]
541
+
542
+ fig = go.Figure()
543
+
544
+ # Raw rewards (faded)
545
+ fig.add_trace(go.Scatter(
546
+ x=ep, y=rw,
547
+ mode="lines",
548
+ name="Raw Reward",
549
+ line=dict(color=COLORS["primary"], width=0.8),
550
+ opacity=0.3,
551
+ ))
552
+
553
+ # Rolling mean
554
+ fig.add_trace(go.Scatter(
555
+ x=rolling_x, y=rolling,
556
+ mode="lines",
557
+ name=f"Rolling Mean ({window} ep)",
558
+ line=dict(color=COLORS["accent"], width=2.5),
559
+ ))
560
+
561
+ fig.update_layout(
562
+ **PLOTLY_LAYOUT,
563
+ height=450,
564
+ title="HAPPO Training - Episode Rewards (IEEE 13-Bus VVC)",
565
+ xaxis_title="Episode",
566
+ yaxis_title="Total Episode Reward",
567
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
568
+ )
569
+ return fig
570
+
571
+
572
+ def plot_agent_policy_loss() -> go.Figure:
573
+ """Plot per-agent policy loss comparison (6 agents)."""
574
+ t = TRAINING_DATA
575
+ ep = t["episodes"]
576
+ losses = t["agent_policy_loss"]
577
+ window = 30
578
+
579
+ fig = go.Figure()
580
+
581
+ for i in range(6):
582
+ raw = losses[:, i]
583
+ smooth = np.convolve(raw, np.ones(window) / window, mode="valid")
584
+ fig.add_trace(go.Scatter(
585
+ x=ep[window - 1:], y=smooth,
586
+ mode="lines",
587
+ name=f"Agent {i}",
588
+ line=dict(color=COLORS["agents"][i], width=2),
589
+ ))
590
+
591
+ fig.update_layout(
592
+ **PLOTLY_LAYOUT,
593
+ height=450,
594
+ title="Per-Agent Policy Loss (Smoothed)",
595
+ xaxis_title="Episode",
596
+ yaxis_title="Policy Loss",
597
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="center", x=0.5),
598
+ )
599
+ return fig
600
+
601
+
602
+ def plot_power_loss_reduction() -> go.Figure:
603
+ """Plot power loss (kW) reduction over training."""
604
+ t = TRAINING_DATA
605
+ ep = t["episodes"]
606
+ pl = t["power_loss_kw"]
607
+ window = 50
608
+
609
+ rolling = np.convolve(pl, np.ones(window) / window, mode="valid")
610
+ rolling_x = ep[window - 1:]
611
+
612
+ fig = go.Figure()
613
+
614
+ fig.add_trace(go.Scatter(
615
+ x=ep, y=pl,
616
+ mode="lines",
617
+ name="Raw",
618
+ line=dict(color=COLORS["danger"], width=0.8),
619
+ opacity=0.25,
620
+ ))
621
+
622
+ fig.add_trace(go.Scatter(
623
+ x=rolling_x, y=rolling,
624
+ mode="lines",
625
+ name=f"Rolling Mean ({window} ep)",
626
+ line=dict(color=COLORS["success"], width=2.5),
627
+ ))
628
+
629
+ # Initial and final annotations
630
+ fig.add_annotation(
631
+ x=0, y=pl[0],
632
+ text=f"Initial: {pl[0]:.0f} kW",
633
+ showarrow=True, arrowhead=2,
634
+ font=dict(color=COLORS["danger"]),
635
+ arrowcolor=COLORS["danger"],
636
+ )
637
+ fig.add_annotation(
638
+ x=1950, y=rolling[-50],
639
+ text=f"Converged: {rolling[-50]:.0f} kW",
640
+ showarrow=True, arrowhead=2,
641
+ font=dict(color=COLORS["success"]),
642
+ arrowcolor=COLORS["success"],
643
+ )
644
+
645
+ fig.update_layout(
646
+ **PLOTLY_LAYOUT,
647
+ height=450,
648
+ title="Distribution Power Loss Reduction During Training",
649
+ xaxis_title="Episode",
650
+ yaxis_title="Power Loss (kW)",
651
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
652
+ )
653
+ return fig
654
+
655
+
656
+ # ============================================================
657
+ # Gradio Application
658
+ # ============================================================
659
+
660
+
661
+ def build_app() -> gr.Blocks:
662
+ """Construct the Gradio Blocks application with 5 tabs."""
663
+ with gr.Blocks(
664
+ title="PowerZoo VVC - Volt-VAR Control Demo",
665
+ theme=gr.themes.Soft(primary_hue="indigo"),
666
+ css="""
667
+ .footer-text {
668
+ text-align: center;
669
+ color: #94A3B8;
670
+ font-size: 0.85em;
671
+ padding: 16px 0;
672
+ }
673
+ """,
674
+ ) as app:
675
+ # Header
676
+ gr.Markdown(
677
+ """
678
+ # PowerZoo VVC: Volt-VAR Control Environment
679
+ **6 Agents** · **24 Steps/Episode** · **Mixed Action Space** · **IEEE Distribution Systems**
680
+ """
681
+ )
682
+
683
+ with gr.Tabs():
684
+ # ================================================================
685
+ # Tab 1: Overview
686
+ # ================================================================
687
+ with gr.Tab("Overview"):
688
+ gr.Markdown(
689
+ """
690
+ ## Environment Description
691
+
692
+ The **VVC (Volt-VAR Control)** environment simulates real-time voltage and
693
+ reactive power management on IEEE distribution networks using OpenDSS as the
694
+ power flow backend. Agents cooperatively control capacitor banks, voltage
695
+ regulators, battery energy storage systems, and PV inverters to minimize
696
+ power losses while maintaining voltage within ANSI limits (0.95-1.05 pu).
697
+
698
+ Each episode spans **24 hourly time steps** (one day). The environment supports
699
+ **6 homogeneous agents**, each responsible for a subset of controllable devices.
700
+ The multi-agent formulation enables scalable control on large distribution networks
701
+ where centralized optimization becomes intractable.
702
+
703
+ ### Key Specifications
704
+ """
705
+ )
706
+
707
+ # Specs table
708
+ specs_df = pd.DataFrame([
709
+ {"Parameter": "Agents", "Value": "6 (homogeneous)"},
710
+ {"Parameter": "Episode Length", "Value": "24 steps (hourly)"},
711
+ {"Parameter": "Action Space", "Value": "Mixed: discrete (cap/reg) + continuous (bat/PV)"},
712
+ {"Parameter": "Observation", "Value": "Bus voltages, power flows, device states, load/PV profiles"},
713
+ {"Parameter": "Reward", "Value": "power_loss + voltage_violation + control_penalty"},
714
+ {"Parameter": "Backend", "Value": "OpenDSS via dss-python"},
715
+ {"Parameter": "Algorithms", "Value": "HAPPO, MAPPO, HATRPO, HADDPG, HASAC, QMix, ..."},
716
+ ])
717
+ gr.Dataframe(
718
+ value=specs_df,
719
+ label="Environment Specifications",
720
+ interactive=False,
721
+ )
722
+
723
+ gr.Markdown(
724
+ """
725
+ ### Supported IEEE Systems
726
+
727
+ | System | Buses | Branches | Loads | Generators | Use Case |
728
+ |--------|-------|----------|-------|------------|----------|
729
+ | **13-Bus** | 13 | 12 | 9 | 1 | Rapid prototyping, algorithm development |
730
+ | **34-Bus** | 34 | 33 | 20 | 1 | Medium-scale validation with PV variants |
731
+ | **123-Bus** | 123 | 122 | 85 | 1 | Large-scale scalability testing |
732
+
733
+ ### Action Space Detail
734
+
735
+ | Device | Type | Range | Description |
736
+ |--------|------|-------|-------------|
737
+ | Capacitor | Discrete | {0, 1} | Switch on/off |
738
+ | Regulator | Discrete | {0, ..., 16} | Tap position |
739
+ | Battery | Continuous | [-1, 1] | Charge/discharge rate |
740
+ | PV Inverter | Continuous | [0, 1] | Curtailment ratio |
741
+
742
+ ### Links
743
+
744
+ [GitHub Repository](https://github.com/XJTU-RL/PowerZoo) ·
745
+ [Documentation](https://xjtu-rl.github.io/PowerZoo/) ·
746
+ IEEE Transactions on Smart Grid, 2025
747
+ """
748
+ )
749
+
750
+ # ================================================================
751
+ # Tab 2: Voltage Profile
752
+ # ================================================================
753
+ with gr.Tab("Voltage Profile"):
754
+ gr.Markdown(
755
+ """
756
+ ## IEEE 13-Bus Voltage Profile
757
+
758
+ Explore bus voltage magnitudes across 24 hourly steps. Bars are colored by
759
+ voltage status: **green** (normal, 0.95-1.05 pu), **yellow** (warning,
760
+ 0.93-0.95 or 1.05-1.07 pu), **red** (violation). During midday, PV injection
761
+ raises upstream voltages; during evening peak, heavy load causes voltage sag on
762
+ downstream buses.
763
+ """
764
+ )
765
+
766
+ step_dropdown = gr.Dropdown(
767
+ choices=list(range(24)),
768
+ value=12,
769
+ label="Select Hour (0-23)",
770
+ )
771
+ voltage_plot = gr.Plot(value=plot_voltage_profile(12))
772
+
773
+ step_dropdown.change(
774
+ fn=plot_voltage_profile,
775
+ inputs=step_dropdown,
776
+ outputs=voltage_plot,
777
+ )
778
+
779
+ # ================================================================
780
+ # Tab 3: Device Schedule
781
+ # ================================================================
782
+ with gr.Tab("Device Schedule"):
783
+ gr.Markdown(
784
+ """
785
+ ## 24-Hour Device Operation Schedule
786
+
787
+ Visualize how 6 agents coordinate device operations across a full day.
788
+ - **Capacitors**: Discrete on/off switching to inject reactive power
789
+ - **Regulators**: Tap adjustments (0-16) to regulate bus voltage
790
+ - **Battery**: Charges from PV midday, discharges during evening peak
791
+ - **PV Inverter**: Curtailment during overvoltage conditions (shaded area = curtailed)
792
+ """
793
+ )
794
+
795
+ device_plot = gr.Plot(value=plot_device_schedule())
796
+
797
+ # ================================================================
798
+ # Tab 4: Reward Analysis
799
+ # ================================================================
800
+ with gr.Tab("Reward Analysis"):
801
+ gr.Markdown(
802
+ """
803
+ ## Episode Reward Decomposition
804
+
805
+ The VVC reward function has three components, all negative (closer to zero is better):
806
+ - **Power Loss** (blue): Penalizes distribution line losses
807
+ - **Voltage Violation** (red): Penalizes buses outside ANSI voltage limits
808
+ - **Control Penalty** (orange): Penalizes excessive device switching
809
+
810
+ The stacked bars show per-step decomposition. The cyan line tracks
811
+ cumulative reward across the episode.
812
+ """
813
+ )
814
+
815
+ reward_plot = gr.Plot(value=plot_reward_analysis())
816
+
817
+ # ================================================================
818
+ # Tab 5: Training Dashboard
819
+ # ================================================================
820
+ with gr.Tab("Training Dashboard"):
821
+ gr.Markdown(
822
+ """
823
+ ## HAPPO Training on IEEE 13-Bus VVC
824
+
825
+ Synthetic training curves demonstrating HAPPO algorithm convergence on the
826
+ VVC environment (2000 episodes, 6 agents, MLP policy).
827
+ """
828
+ )
829
+
830
+ gr.Markdown("### Episode Reward Curve")
831
+ training_reward_plot = gr.Plot(value=plot_training_rewards())
832
+
833
+ gr.Markdown("### Per-Agent Policy Loss")
834
+ agent_loss_plot = gr.Plot(value=plot_agent_policy_loss())
835
+
836
+ gr.Markdown("### Power Loss Reduction")
837
+ power_loss_plot = gr.Plot(value=plot_power_loss_reduction())
838
+
839
+ # Footer
840
+ gr.Markdown(
841
+ """
842
+ ---
843
+ <p class="footer-text">
844
+ PowerZoo · MIT License · XJTU-RL · IEEE TSG 2025
845
+ </p>
846
+ """,
847
+ )
848
+
849
+ return app
850
+
851
+
852
+ # ============================================================
853
+ # Launch
854
+ # ============================================================
855
+ if __name__ == "__main__":
856
+ app = build_app()
857
+ app.launch(server_name="0.0.0.0", server_port=7860, share=False)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio==4.44.1
2
+ plotly>=5.18.0
3
+ pandas>=2.0.0
4
+ numpy>=1.24.0