-
Notifications
You must be signed in to change notification settings - Fork 14
Open
Labels
bugSomething isn't workingSomething isn't working
Description
Environment
- grid2op 1.12.3
- lightsim2grid 0.12.2
- Oracle Linux Server 8.10
uv venv --python 3.12.9
uv pip install grid2op==1.12.3 lightsim2grid==0.12.2
Bug description
When using lightsim2grid, it seems that obs.simulate can have side effects and 'break' the initial observation we are working on.
This is not the case when using pypowsybl2grid or pandapower.
In the example bellow, we intentionally create a 'bad' forecast timeserie, that diverges at timestep=9. Once this is done, we can not go back in time anymore.
How to reproduce
Code snippet
import grid2op
import numpy as np
from datetime import timedelta
from lightsim2grid import LightSimBackend
from grid2op.Chronics import FromNPY
from grid2op.Exceptions import ChronicsError
from typing import Optional
class FromNPYMultiHorizon(FromNPY):
"""
Extension of FromNPY supporting multi-horizon forecasts.
Forecast arrays can be:
(T, N) -> automatically converted to (T, 1, N)
(T, H, N) -> used directly
Where:
T = number of timesteps
H = number of forecast horizons
N = number of loads / generators
"""
MULTI_CHRONICS = False
def __init__(
self,
*args,
load_p_forecast: Optional[np.ndarray] = None,
load_q_forecast: Optional[np.ndarray] = None,
prod_p_forecast: Optional[np.ndarray] = None,
prod_v_forecast: Optional[np.ndarray] = None,
**kwargs,
):
# Prevent parent from building recursive forecast object
super().__init__(
*args,
load_p_forecast=None,
load_q_forecast=None,
prod_p_forecast=None,
prod_v_forecast=None,
**kwargs,
)
# Format forecast arrays
self._load_p_forecast = self._format_forecast_array(load_p_forecast)
self._load_q_forecast = self._format_forecast_array(load_q_forecast)
self._prod_p_forecast = self._format_forecast_array(prod_p_forecast)
self._prod_v_forecast = self._format_forecast_array(prod_v_forecast)
# Determine number of horizons
if self._load_p_forecast is not None:
self.n_forecast_horizons = self._load_p_forecast.shape[1]
else:
self.n_forecast_horizons = 0
@staticmethod
def _format_forecast_array(arr: Optional[np.ndarray]) -> Optional[np.ndarray]:
"""
Convert forecast array to shape (T, H, N)
Accepts:
(T, N)
(T, H, N)
"""
if arr is None:
return None
if arr.ndim == 2:
# (T, N) → (T, 1, N)
return arr[:, np.newaxis, :]
if arr.ndim == 3:
return arr
raise ChronicsError("Forecast arrays must have shape (T, N) or (T, H, N)")
def forecasts(self):
"""
Return list of (datetime, dict) for each horizon.
"""
if self._load_p_forecast is None:
return []
t = self.current_index
if t >= self._load_p_forecast.shape[0]:
return []
results = []
for h in range(self.n_forecast_horizons):
dict_ = {}
if self._load_p_forecast is not None:
dict_["load_p"] = 1.0 * self._load_p_forecast[t, h, :]
if self._load_q_forecast is not None:
dict_["load_q"] = 1.0 * self._load_q_forecast[t, h, :]
if self._prod_p_forecast is not None:
dict_["prod_p"] = 1.0 * self._prod_p_forecast[t, h, :]
if self._prod_v_forecast is not None:
dict_["prod_v"] = 1.0 * self._prod_v_forecast[t, h, :]
forecast_datetime = self.current_datetime + (h + 1) * self.time_interval
results.append((forecast_datetime, {"injection": dict_}))
return results
def change_forecasts(
self,
new_load_p: Optional[np.ndarray] = None,
new_load_q: Optional[np.ndarray] = None,
new_prod_p: Optional[np.ndarray] = None,
new_prod_v: Optional[np.ndarray] = None,
):
"""
Update forecast arrays (effective after env.reset()).
"""
if new_load_p is not None:
self._load_p_forecast = self._format_forecast_array(new_load_p)
if new_load_q is not None:
self._load_q_forecast = self._format_forecast_array(new_load_q)
if new_prod_p is not None:
self._prod_p_forecast = self._format_forecast_array(new_prod_p)
if new_prod_v is not None:
self._prod_v_forecast = self._format_forecast_array(new_prod_v)
if self._load_p_forecast is not None:
self.n_forecast_horizons = self._load_p_forecast.shape[1]
else:
self.n_forecast_horizons = 0
def check_validity(self, backend=None):
super().check_validity(backend)
if self._load_p_forecast is not None:
assert self._load_p_forecast.shape[0] == self._load_p.shape[0]
assert self._load_p_forecast.shape[2] == self.n_load
if self._prod_p_forecast is not None:
assert self._prod_p_forecast.shape[0] == self._prod_p.shape[0]
assert self._prod_p_forecast.shape[2] == self.n_gen
env = grid2op.make(
"l2rpn_case14_sandbox",
backend=LightSimBackend(),
)
init_obs = env.reset()
# Artificially create a timeseries :
load_p, load_q, prod_p, prod_v = (
init_obs.load_p,
init_obs.load_q,
init_obs.prod_p,
init_obs.prod_v,
)
load_p = np.concatenate([[load_p] * 48], axis=0)
load_q = np.concatenate([[load_q] * 48], axis=0)
prod_p = np.concatenate([[prod_p] * 48], axis=0)
prod_v = np.concatenate([[prod_v] * 48], axis=0)
load_p_fc = load_p
load_q_fc = load_q
prod_p_fc = prod_p
prod_v_fc = prod_v
# create a fake moving timeserie that will create a DC divergence (too high load)
load_p_fake = (load_p + (np.linspace(0, 1, len(load_p)) * 100).reshape(-1, 1)).round(1)
T, N = load_p_fake.shape
H = 48
indices = np.arange(T)[:, None] + np.arange(1, H + 1)[None, :]
indices = np.clip(indices, 0, T - 1)
load_p_fc = load_p_fake[indices]
load_q_fc = load_q[indices]
prod_p_fc = prod_p[indices]
prod_v_fc = prod_v[indices]
load_p_fc[1:, :, :] = (
0 # Forecast available at t =0. We only need the first row for each forecast
)
env = grid2op.make(
"l2rpn_case14_sandbox",
backend=LightSimBackend(),
allow_detachment=True,
n_busbar=6,
chronics_class=FromNPYMultiHorizon,
data_feeding_kwargs={
"i_start": 0,
"i_end": 48,
"load_p": load_p_fake,
"load_q": load_q,
"prod_p": prod_p,
"prod_v": prod_v,
"load_p_forecast": load_p_fc,
"load_q_forecast": load_q_fc,
"prod_p_forecast": prod_p_fc,
"prod_v_forecast": prod_v_fc,
"h_forecast": [i * 30 for i in range(1, 48)],
"time_interval": timedelta(minutes=30),
},
)
env.chronics_handler.change_chronics(
new_load_p=load_p_fake,
new_load_q=load_q,
new_prod_p=prod_p,
new_prod_v=prod_v,
)
env.chronics_handler.change_forecasts(
new_load_p=load_p_fc,
new_load_q=load_q_fc,
new_prod_p=prod_p_fc,
new_prod_v=prod_v_fc,
)
##
if __name__ == "__main__":
obs_init = env.reset()
print(obs_init.get_time_stamp())
print(obs_init.load_p)
# OK
obs_step, *_ = env.step(env.action_space())
print(obs_step.get_time_stamp())
print(obs_step.load_p)
# OK
obs_simulate, *_ = obs_init.simulate(env.action_space(), time_step=8) # t = 0 ... 8 is Okay !
print(obs_simulate.get_time_stamp())
print(obs_simulate.load_p)
print(obs_simulate.rho)
# OK
obs_simulate, *_ = obs_init.simulate(env.action_space(), time_step=5) # t = 0 ... 8 is Okay !
print(obs_simulate.get_time_stamp())
print(obs_simulate.load_p)
print(obs_simulate.rho)
# After this NOK
obs_simulate, *_ = obs_init.simulate(env.action_space(), time_step=9)
print(obs_simulate.get_time_stamp())
print(obs_simulate.load_p)
print(obs_simulate.rho)
obs_simulate, *_ = obs_init.simulate(env.action_space(), time_step=5)
print(obs_simulate.get_time_stamp())
print(obs_simulate.load_p)
print(obs_simulate.rho)Current output
2019-01-01 00:30:00
[21.9 85.8 44.3 6.9 11.9 28.5 8.8 3.5 5.4 12.6 14.4]
2019-01-01 01:00:00
[24. 87.9 46.4 9. 14. 30.6 10.9 5.6 7.5 14.7 16.5]
2019-01-01 04:30:00
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
2019-01-01 03:00:00
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
2019-01-01 05:00:00
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
2019-01-01 03:00:00
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
Expected output
This output was created using Pandapower backend:
2019-01-01 00:30:00
[21.9 85.8 44.3 6.9 11.9 28.5 8.8 3.5 5.4 12.6 14.4]
2019-01-01 01:00:00
[24. 87.9 46.4 9. 14. 30.6 10.9 5.6 7.5 14.7 16.5]
2019-01-01 04:30:00
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
2019-01-01 03:00:00
[32.5 96.4 54.9 17.5 22.5 39.1 19.4 14.1 16. 23.2 25. ]
[1.0019906 0.7419449 0.4845092 0.44415575 1.4045746 0.420179
0.50475854 0.80372405 0.9490499 1.1287203 1.2295688 0.7897864
0.34381983 0.4001435 0.4910553 0.9341112 0.99194753 1.8852744
0.57754666 0.86008847]
2019-01-01 05:00:00
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
2019-01-01 03:00:00
[32.5 96.4 54.9 17.5 22.5 39.1 19.4 14.1 16. 23.2 25. ]
[1.0019906 0.7419449 0.4845092 0.44415575 1.4045746 0.420179
0.50475854 0.80372405 0.9490499 1.1287203 1.2295688 0.7897864
0.34381983 0.4001435 0.4910553 0.9341112 0.99194753 1.8852744
0.57754666 0.86008847]
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't working