77ethers commited on
Commit
bafd9fc
·
verified ·
1 Parent(s): 164be01

Upload folder using huggingface_hub

Browse files
gridops/server/environment.py CHANGED
@@ -107,6 +107,10 @@ class GridOpsEnvironment(Environment):
107
  )
108
 
109
  h = self._micro.hour
 
 
 
 
110
  result = physics.step(
111
  self._micro,
112
  battery_dispatch_norm=action.battery_dispatch,
@@ -116,6 +120,7 @@ class GridOpsEnvironment(Environment):
116
  demand_kw=float(self._demand[h]),
117
  grid_price=float(self._price[h]),
118
  diesel_fuel_cap=self._cfg.diesel_fuel_capacity * DIESEL_TANK_KWH,
 
119
  )
120
 
121
  self._last_flows = result.flows
@@ -137,7 +142,8 @@ class GridOpsEnvironment(Environment):
137
 
138
  if result.done:
139
  self._grade = grade_episode(
140
- self._micro, self._demand, self._solar, self._price
 
141
  )
142
 
143
  obs = self._make_observation(
 
107
  )
108
 
109
  h = self._micro.hour
110
+ # Check if grid is available this hour (outage = islanding mode)
111
+ outage_hours = self._cfg.grid_outage_hours or []
112
+ grid_up = h not in outage_hours
113
+
114
  result = physics.step(
115
  self._micro,
116
  battery_dispatch_norm=action.battery_dispatch,
 
120
  demand_kw=float(self._demand[h]),
121
  grid_price=float(self._price[h]),
122
  diesel_fuel_cap=self._cfg.diesel_fuel_capacity * DIESEL_TANK_KWH,
123
+ grid_available=grid_up,
124
  )
125
 
126
  self._last_flows = result.flows
 
142
 
143
  if result.done:
144
  self._grade = grade_episode(
145
+ self._micro, self._demand, self._solar, self._price,
146
+ grid_outage_hours=self._cfg.grid_outage_hours,
147
  )
148
 
149
  obs = self._make_observation(
gridops/server/static/index.html CHANGED
@@ -577,7 +577,7 @@
577
  <div class="control-group">
578
  <label>&#127968; Ask residents to cut usage <span class="val" id="shedVal">none</span></label>
579
  <input type="range" id="shedSlider" min="0" max="100" value="0" step="1">
580
- <div style="font-size:9px;color:var(--yellow)">50% of shed demand rebounds next hour!</div>
581
  </div>
582
  </div>
583
 
 
577
  <div class="control-group">
578
  <label>&#127968; Ask residents to cut usage <span class="val" id="shedVal">none</span></label>
579
  <input type="range" id="shedSlider" min="0" max="100" value="0" step="1">
580
+ <div style="font-size:9px;color:var(--yellow)">100% rebounds next hour! Rs 40/kWh penalty.</div>
581
  </div>
582
  </div>
583
 
gridops/simulation/physics.py CHANGED
@@ -30,7 +30,7 @@ DIESEL_MAX_KW = 100.0
30
  DIESEL_COST_PER_KWH = 25.0
31
  DIESEL_STARTUP_COST = 100.0 # Rs, one-time when turning on from off
32
  DEMAND_SHED_MAX_FRAC = 0.20
33
- SHED_REBOUND_FRAC = 0.50 # 50% of shed energy rebounds next hour
34
  DIESEL_TANK_KWH = 2400.0 # total fuel capacity
35
  VOLL = 150.0 # Value of Lost Load (Rs/kWh)
36
  DT = 1.0 # 1 hour per step
@@ -102,6 +102,7 @@ def step(
102
  demand_kw: float,
103
  grid_price: float,
104
  diesel_fuel_cap: float = DIESEL_TANK_KWH,
 
105
  ) -> StepResult:
106
  """
107
  Advance the microgrid by one hour.
@@ -119,7 +120,7 @@ def step(
119
  shed_frac = float(np.clip(shed_norm, 0, 1)) * DEMAND_SHED_MAX_FRAC
120
 
121
  # ── Demand (with shedding rebound from last step) ────────────────
122
- actual_demand = demand_kw + state.shed_rebound_kwh
123
  effective_demand = actual_demand * (1.0 - shed_frac)
124
  shed_kwh = actual_demand * shed_frac * DT
125
  state.shed_rebound_kwh = shed_kwh * SHED_REBOUND_FRAC # 50% rebounds next hour
@@ -152,16 +153,17 @@ def step(
152
  # ── Grid as slack variable ───────────────────────────────────────
153
  # residual = what the community still needs after solar + battery + diesel
154
  # positive → grid must import; negative → surplus exported
 
155
  residual = effective_demand - solar_kw - delivered_kw - diesel_kw
156
- grid_kw = float(np.clip(residual, -GRID_MAX_KW, GRID_MAX_KW))
157
 
158
  # ── Blackout / curtailment detection ─────────────────────────────
159
  blackout_kwh = 0.0
160
  curtailed_kw = 0.0
161
- if residual > GRID_MAX_KW:
162
- blackout_kwh = (residual - GRID_MAX_KW) * DT
163
- elif residual < -GRID_MAX_KW:
164
- curtailed_kw = abs(residual) - GRID_MAX_KW # excess that can't be exported
165
 
166
  # ── Build flow snapshot ──────────────────────────────────────────
167
  grid_import = max(0.0, grid_kw)
@@ -208,8 +210,9 @@ def step(
208
  # VoLL penalty (replaces hard reliability gate)
209
  step_cost += VOLL * blackout_kwh
210
 
211
- # Shedding penalty (small comfort cost — Rs 5/kWh shed)
212
- step_cost += 5.0 * shed_kwh
 
213
 
214
  # ── Fuel accounting ──────────────────────────────────────────────
215
  state.diesel_fuel_kwh -= diesel_kw * DT
@@ -235,7 +238,7 @@ def step(
235
 
236
  # ── Narration ────────────────────────────────────────────────────
237
  narration = _narrate(state, solar_kw, actual_demand, grid_price, blackout_kwh,
238
- diesel_kw, shed_frac, grid_kw, delivered_kw)
239
 
240
  return StepResult(state=state, reward=reward, done=done, narration=narration, flows=flows)
241
 
@@ -250,6 +253,7 @@ def _narrate(
250
  shed: float,
251
  grid_kw: float,
252
  battery_kw: float,
 
253
  ) -> str:
254
  """Generate a short human-readable situation summary."""
255
  START_HOUR = 6
@@ -260,6 +264,9 @@ def _narrate(
260
 
261
  parts = [f"Day {day}, {hour_of_day:02d}:00."]
262
 
 
 
 
263
  if blackout > 0:
264
  parts.append(f"BLACKOUT: {blackout:.0f} kWh unmet!")
265
  elif demand > 200:
 
30
  DIESEL_COST_PER_KWH = 25.0
31
  DIESEL_STARTUP_COST = 100.0 # Rs, one-time when turning on from off
32
  DEMAND_SHED_MAX_FRAC = 0.20
33
+ SHED_REBOUND_FRAC = 1.00 # 100% of shed energy rebounds next hour (deferred, not destroyed)
34
  DIESEL_TANK_KWH = 2400.0 # total fuel capacity
35
  VOLL = 150.0 # Value of Lost Load (Rs/kWh)
36
  DT = 1.0 # 1 hour per step
 
102
  demand_kw: float,
103
  grid_price: float,
104
  diesel_fuel_cap: float = DIESEL_TANK_KWH,
105
+ grid_available: bool = True,
106
  ) -> StepResult:
107
  """
108
  Advance the microgrid by one hour.
 
120
  shed_frac = float(np.clip(shed_norm, 0, 1)) * DEMAND_SHED_MAX_FRAC
121
 
122
  # ── Demand (with shedding rebound from last step) ────────────────
123
+ actual_demand = demand_kw + state.shed_rebound_kwh / DT # rebound is kWh, convert to kW
124
  effective_demand = actual_demand * (1.0 - shed_frac)
125
  shed_kwh = actual_demand * shed_frac * DT
126
  state.shed_rebound_kwh = shed_kwh * SHED_REBOUND_FRAC # 50% rebounds next hour
 
153
  # ── Grid as slack variable ───────────────────────────────────────
154
  # residual = what the community still needs after solar + battery + diesel
155
  # positive → grid must import; negative → surplus exported
156
+ grid_cap = GRID_MAX_KW if grid_available else 0.0
157
  residual = effective_demand - solar_kw - delivered_kw - diesel_kw
158
+ grid_kw = float(np.clip(residual, -grid_cap, grid_cap))
159
 
160
  # ── Blackout / curtailment detection ─────────────────────────────
161
  blackout_kwh = 0.0
162
  curtailed_kw = 0.0
163
+ if residual > grid_cap:
164
+ blackout_kwh = (residual - grid_cap) * DT
165
+ elif residual < -grid_cap:
166
+ curtailed_kw = abs(residual) - grid_cap # excess that can't be exported
167
 
168
  # ── Build flow snapshot ──────────────────────────────────────────
169
  grid_import = max(0.0, grid_kw)
 
210
  # VoLL penalty (replaces hard reliability gate)
211
  step_cost += VOLL * blackout_kwh
212
 
213
+ # Shedding penalty (comfort + political cost — Rs 40/kWh shed)
214
+ # More expensive than diesel (Rs 25), so only used as true emergency
215
+ step_cost += 40.0 * shed_kwh
216
 
217
  # ── Fuel accounting ──────────────────────────────────────────────
218
  state.diesel_fuel_kwh -= diesel_kw * DT
 
238
 
239
  # ── Narration ────────────────────────────────────────────────────
240
  narration = _narrate(state, solar_kw, actual_demand, grid_price, blackout_kwh,
241
+ diesel_kw, shed_frac, grid_kw, delivered_kw, grid_available)
242
 
243
  return StepResult(state=state, reward=reward, done=done, narration=narration, flows=flows)
244
 
 
253
  shed: float,
254
  grid_kw: float,
255
  battery_kw: float,
256
+ grid_available: bool = True,
257
  ) -> str:
258
  """Generate a short human-readable situation summary."""
259
  START_HOUR = 6
 
264
 
265
  parts = [f"Day {day}, {hour_of_day:02d}:00."]
266
 
267
+ if not grid_available:
268
+ parts.append("GRID OUTAGE — islanding mode! No grid import/export.")
269
+
270
  if blackout > 0:
271
  parts.append(f"BLACKOUT: {blackout:.0f} kWh unmet!")
272
  elif demand > 200:
gridops/simulation/scenarios.py CHANGED
@@ -27,6 +27,7 @@ class ScenarioConfig:
27
  cloud_hours: list[int] | None = None # hours with intermittent clouds
28
  diesel_fuel_capacity: float = 1.0 # 1.0 = full tank (800 kWh worth)
29
  forecast_noise: float = 0.15 # ±15 % Gaussian noise on forecasts
 
30
 
31
 
32
  # ── Demand ───────────────────────────────────────────────────────────────
 
27
  cloud_hours: list[int] | None = None # hours with intermittent clouds
28
  diesel_fuel_capacity: float = 1.0 # 1.0 = full tank (800 kWh worth)
29
  forecast_noise: float = 0.15 # ±15 % Gaussian noise on forecasts
30
+ grid_outage_hours: list[int] | None = None # hours where grid cap drops to 0 (islanding)
31
 
32
 
33
  # ── Demand ───────────────────────────────────────────────────────────────
gridops/tasks/definitions.py CHANGED
@@ -1,9 +1,9 @@
1
  """
2
  Three task configurations with escalating difficulty.
3
 
4
- Task 1: Normal Summer (easy)
5
- Task 2: Heatwave + Clouds (medium)
6
- Task 3: Extreme Crisis (hard)
7
  """
8
 
9
  from gridops.simulation.scenarios import ScenarioConfig
@@ -19,21 +19,30 @@ TASK_1_NORMAL = ScenarioConfig(
19
  cloud_hours=None,
20
  diesel_fuel_capacity=1.0,
21
  forecast_noise=0.15,
 
22
  )
23
 
 
 
 
 
24
  TASK_2_HEATWAVE = ScenarioConfig(
25
  demand_multiplier=1.3,
26
  solar_multiplier=1.0,
27
- price_floor=5.0,
28
  price_ceiling=15.0,
29
- price_spike_hour=44, # Day 2, 20:00 (hour 44)
30
- price_spike_value=18.0,
31
  heatwave_start_hour=24, # Day 2 start
32
- cloud_hours=[30, 31, 36, 37, 54, 55], # intermittent Day 2-3
33
  diesel_fuel_capacity=1.0,
34
  forecast_noise=0.15,
 
35
  )
36
 
 
 
 
37
  TASK_3_CRISIS = ScenarioConfig(
38
  demand_multiplier=1.5,
39
  solar_multiplier=0.7,
@@ -41,10 +50,11 @@ TASK_3_CRISIS = ScenarioConfig(
41
  price_ceiling=20.0,
42
  price_spike_hour=44,
43
  price_spike_value=20.0,
44
- heatwave_start_hour=0, # heatwave from the start
45
  cloud_hours=list(range(8, 16)) + list(range(32, 40)) + list(range(56, 64)),
46
  diesel_fuel_capacity=0.33, # 800 kWh ≈ 8 hrs at 100 kW
47
  forecast_noise=0.15,
 
48
  )
49
 
50
  TASKS = {
 
1
  """
2
  Three task configurations with escalating difficulty.
3
 
4
+ Task 1: Normal Summer (easy) — basic arbitrage
5
+ Task 2: Heatwave + Price Spike (medium) — forces forecasting / temporal planning
6
+ Task 3: Extreme Crisis + Grid Outage (hard) — forces islanding / constraint management
7
  """
8
 
9
  from gridops.simulation.scenarios import ScenarioConfig
 
19
  cloud_hours=None,
20
  diesel_fuel_capacity=1.0,
21
  forecast_noise=0.15,
22
+ grid_outage_hours=None,
23
  )
24
 
25
+ # Task 2: Heatwave + severe evening price spikes on Day 2-3.
26
+ # Mid-day prices dip (solar glut), then spike to Rs 18-20 at evening.
27
+ # A greedy agent discharges battery mid-day; an RL agent reads the
28
+ # 4-hour forecast and HOLDS charge for the evening spike.
29
  TASK_2_HEATWAVE = ScenarioConfig(
30
  demand_multiplier=1.3,
31
  solar_multiplier=1.0,
32
+ price_floor=3.0,
33
  price_ceiling=15.0,
34
+ price_spike_hour=36, # Day 2, ~18:00 (step 36 = hour 12 of day 2)
35
+ price_spike_value=20.0, # severe spike — hold battery for this
36
  heatwave_start_hour=24, # Day 2 start
37
+ cloud_hours=[30, 31, 36, 37, 54, 55],
38
  diesel_fuel_capacity=1.0,
39
  forecast_noise=0.15,
40
+ grid_outage_hours=None,
41
  )
42
 
43
+ # Task 3: Full crisis + 6-hour grid outage on Day 2 evening.
44
+ # Grid cap drops to 0 kW during outage — agent must survive on
45
+ # battery + diesel + shedding alone. Tests true islanding capability.
46
  TASK_3_CRISIS = ScenarioConfig(
47
  demand_multiplier=1.5,
48
  solar_multiplier=0.7,
 
50
  price_ceiling=20.0,
51
  price_spike_hour=44,
52
  price_spike_value=20.0,
53
+ heatwave_start_hour=0,
54
  cloud_hours=list(range(8, 16)) + list(range(32, 40)) + list(range(56, 64)),
55
  diesel_fuel_capacity=0.33, # 800 kWh ≈ 8 hrs at 100 kW
56
  forecast_noise=0.15,
57
+ grid_outage_hours=list(range(30, 36)), # 6-hour outage: Day 2, ~12:00-18:00
58
  )
59
 
60
  TASKS = {
gridops/tasks/graders.py CHANGED
@@ -28,26 +28,27 @@ def compute_dumb_baseline_cost(
28
  demand_curve: np.ndarray,
29
  solar_curve: np.ndarray,
30
  price_curve: np.ndarray,
 
31
  ) -> float:
32
  """Cost of a dumb baseline: import max grid, no battery/diesel/shedding.
33
 
34
  Where demand > grid + solar, apply VoLL for the blackout.
35
- This is a realistic "no-intelligence" baseline.
36
  """
 
37
  total_cost = 0.0
38
  for h in range(len(demand_curve)):
39
  demand = demand_curve[h]
40
  solar = solar_curve[h]
41
  price = price_curve[h]
 
42
 
43
- # Grid covers what it can (up to 200 kW)
44
  needed_from_grid = max(0.0, demand - solar)
45
- grid_import = min(needed_from_grid, GRID_MAX_KW)
46
- total_cost += price * grid_import # grid cost
47
 
48
- # Any excess demand is a blackout
49
- unmet = max(0.0, needed_from_grid - GRID_MAX_KW)
50
- total_cost += VOLL * unmet # VoLL penalty
51
 
52
  return float(total_cost)
53
 
@@ -57,6 +58,7 @@ def grade_episode(
57
  demand_curve: np.ndarray,
58
  solar_curve: np.ndarray,
59
  price_curve: np.ndarray,
 
60
  ) -> dict:
61
  """
62
  Grade a completed episode. Returns dict with score 0.0-1.0.
@@ -71,7 +73,7 @@ def grade_episode(
71
  reliability = float(np.clip(reliability, 0, 1))
72
 
73
  # Cost efficiency: how much better than dumb baseline
74
- baseline_cost = compute_dumb_baseline_cost(demand_curve, solar_curve, price_curve)
75
  actual_cost = state.cumulative_cost
76
  if baseline_cost > 0:
77
  cost_efficiency = 1.0 - (actual_cost / baseline_cost)
 
28
  demand_curve: np.ndarray,
29
  solar_curve: np.ndarray,
30
  price_curve: np.ndarray,
31
+ grid_outage_hours: list[int] | None = None,
32
  ) -> float:
33
  """Cost of a dumb baseline: import max grid, no battery/diesel/shedding.
34
 
35
  Where demand > grid + solar, apply VoLL for the blackout.
36
+ During grid outages, all non-solar demand is blackout.
37
  """
38
+ outages = set(grid_outage_hours or [])
39
  total_cost = 0.0
40
  for h in range(len(demand_curve)):
41
  demand = demand_curve[h]
42
  solar = solar_curve[h]
43
  price = price_curve[h]
44
+ grid_cap = 0.0 if h in outages else GRID_MAX_KW
45
 
 
46
  needed_from_grid = max(0.0, demand - solar)
47
+ grid_import = min(needed_from_grid, grid_cap)
48
+ total_cost += price * grid_import
49
 
50
+ unmet = max(0.0, needed_from_grid - grid_cap)
51
+ total_cost += VOLL * unmet
 
52
 
53
  return float(total_cost)
54
 
 
58
  demand_curve: np.ndarray,
59
  solar_curve: np.ndarray,
60
  price_curve: np.ndarray,
61
+ grid_outage_hours: list[int] | None = None,
62
  ) -> dict:
63
  """
64
  Grade a completed episode. Returns dict with score 0.0-1.0.
 
73
  reliability = float(np.clip(reliability, 0, 1))
74
 
75
  # Cost efficiency: how much better than dumb baseline
76
+ baseline_cost = compute_dumb_baseline_cost(demand_curve, solar_curve, price_curve, grid_outage_hours)
77
  actual_cost = state.cumulative_cost
78
  if baseline_cost > 0:
79
  cost_efficiency = 1.0 - (actual_cost / baseline_cost)
openenv.yaml CHANGED
@@ -36,4 +36,4 @@ metadata:
36
  observation_space: "12 fields: hour, demand, solar, battery_soc, price, fuel, 3×4h forecasts, cumulative metrics"
37
  episode_length: 72
38
  step_duration: "1 hour"
39
- grading: "Reliability-gated composite: 60% cost savings + 20% reliability + 20% green score"
 
36
  observation_space: "12 fields: hour, demand, solar, battery_soc, price, fuel, 3×4h forecasts, cumulative metrics"
37
  episode_length: 72
38
  step_duration: "1 hour"
39
+ grading: "Composite: 50% cost efficiency + 25% reliability + 25% green score. VoLL Rs 150/kWh blackout penalty."